use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PrioritizationStrategyKind {
Snapshot,
Incremental,
Chunked,
TimeWindow,
}
impl PrioritizationStrategyKind {
pub fn from_extraction(strategy: &super::ExtractionStrategy) -> Self {
match strategy {
super::ExtractionStrategy::Snapshot => Self::Snapshot,
super::ExtractionStrategy::Incremental(_) => Self::Incremental,
super::ExtractionStrategy::Chunked(_) => Self::Chunked,
super::ExtractionStrategy::TimeWindow { .. } => Self::TimeWindow,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CursorQuality {
StrongMonotonic,
WeakTime,
WeakMultiCandidate,
FallbackOnly,
None,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SourceFreshnessHint {
Recent,
Stale,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RecommendationReasonKind {
SmallTable,
LargeTable,
HugeTable,
StrongCursor,
WeakCursor,
NoTimeCursor,
SparseRangeRisk,
ChunkingHeavy,
ReconcileRequired,
FreshnessHintRecent,
SharedSourceHeavyConflict,
LowConfidenceMetadata,
HighRetryRateHistory,
RecentFailureHistory,
SlowHistory,
}
impl RecommendationReasonKind {
pub fn as_str(&self) -> &'static str {
match self {
Self::SmallTable => "small_table",
Self::LargeTable => "large_table",
Self::HugeTable => "huge_table",
Self::StrongCursor => "strong_cursor",
Self::WeakCursor => "weak_cursor",
Self::NoTimeCursor => "no_time_cursor",
Self::SparseRangeRisk => "sparse_range_risk",
Self::ChunkingHeavy => "chunking_heavy",
Self::ReconcileRequired => "reconcile_required",
Self::FreshnessHintRecent => "freshness_hint_recent",
Self::SharedSourceHeavyConflict => "shared_source_heavy_conflict",
Self::LowConfidenceMetadata => "low_confidence_metadata",
Self::HighRetryRateHistory => "high_retry_rate_history",
Self::RecentFailureHistory => "recent_failure_history",
Self::SlowHistory => "slow_history",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RecommendationReason {
pub kind: RecommendationReasonKind,
pub message: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CostClass {
Low,
Medium,
High,
VeryHigh,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RiskClass {
Low,
Medium,
High,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PriorityClass {
Low,
Medium,
High,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExportRecommendation {
pub export_name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_group: Option<String>,
pub priority_score: i32,
pub priority_class: PriorityClass,
pub cost_class: CostClass,
pub risk_class: RiskClass,
pub recommended_wave: u32,
pub isolate_on_source: bool,
pub reasons: Vec<RecommendationReason>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RecommendedWave {
pub wave: u32,
pub exports: Vec<String>,
}
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SourceGroupInfo {
pub name: String,
pub export_names: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CampaignRecommendation {
pub ordered_exports: Vec<ExportRecommendation>,
pub waves: Vec<RecommendedWave>,
pub source_group_warnings: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PrioritizationInputs {
pub export_name: String,
pub source_group: Option<String>,
pub strategy: PrioritizationStrategyKind,
pub estimated_rows: Option<u64>,
pub estimated_size_bytes: Option<u64>,
pub chunk_count: Option<u32>,
pub sparse_range_risk: bool,
pub cursor_quality: CursorQuality,
pub reconcile_required: bool,
pub source_freshness_hint: Option<SourceFreshnessHint>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub history: Option<super::HistorySnapshot>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::IncrementalCursorMode;
use crate::plan::{ChunkedPlan, ExtractionStrategy, IncrementalCursorPlan};
#[test]
fn strategy_kind_from_extraction_variants() {
assert_eq!(
PrioritizationStrategyKind::from_extraction(&ExtractionStrategy::Snapshot),
PrioritizationStrategyKind::Snapshot
);
assert_eq!(
PrioritizationStrategyKind::from_extraction(&ExtractionStrategy::Incremental(
IncrementalCursorPlan {
primary_column: "x".into(),
fallback_column: None,
mode: IncrementalCursorMode::SingleColumn,
}
)),
PrioritizationStrategyKind::Incremental
);
assert_eq!(
PrioritizationStrategyKind::from_extraction(&ExtractionStrategy::Chunked(
ChunkedPlan {
column: "id".into(),
chunk_size: 1,
chunk_count: None,
parallel: 1,
dense: false,
by_days: None,
checkpoint: false,
max_attempts: 1,
}
)),
PrioritizationStrategyKind::Chunked
);
assert_eq!(
PrioritizationStrategyKind::from_extraction(&ExtractionStrategy::TimeWindow {
column: "t".into(),
column_type: crate::plan::TimeColumnType::Timestamp,
days_window: 1,
}),
PrioritizationStrategyKind::TimeWindow
);
}
#[test]
fn json_round_trip_export_recommendation() {
let rec = ExportRecommendation {
export_name: "orders".into(),
source_group: None,
priority_score: 10,
priority_class: PriorityClass::High,
cost_class: CostClass::Medium,
risk_class: RiskClass::Low,
recommended_wave: 1,
isolate_on_source: false,
reasons: vec![RecommendationReason {
kind: RecommendationReasonKind::StrongCursor,
message: "Monotonic cursor configured".into(),
}],
};
let json = serde_json::to_string(&rec).unwrap();
let back: ExportRecommendation = serde_json::from_str(&json).unwrap();
assert_eq!(back, rec);
}
}