1use serde::{Deserialize, Serialize};
4
5use crate::model::EmbeddingModel;
6
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
9#[serde(rename_all = "snake_case")]
10#[non_exhaustive]
11pub enum SkipReason {
12 ContentTooLarge {
14 size: usize,
16 max: usize,
18 },
19 InvalidEncoding(String),
21 ContentDeleted,
23 PermanentApiError(String),
25 ManualSkip(String),
27}
28
29impl std::fmt::Display for SkipReason {
30 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31 match self {
32 SkipReason::ContentTooLarge { size, max } => {
33 write!(f, "content too large: {size} bytes (max {max})")
34 }
35 SkipReason::InvalidEncoding(enc) => write!(f, "invalid encoding: {enc}"),
36 SkipReason::ContentDeleted => write!(f, "content deleted"),
37 SkipReason::PermanentApiError(msg) => write!(f, "permanent API error: {msg}"),
38 SkipReason::ManualSkip(reason) => write!(f, "manually skipped: {reason}"),
39 }
40 }
41}
42
43#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
45#[serde(rename_all = "snake_case")]
46#[non_exhaustive]
47pub enum MigrationState {
48 #[serde(alias = "Planned")]
51 Planned,
52 #[serde(alias = "InProgress")]
54 InProgress {
55 processed: usize,
57 total: usize,
59 #[serde(default)]
61 skipped: usize,
62 },
63 #[serde(alias = "Paused")]
65 Paused {
66 processed: usize,
68 total: usize,
70 #[serde(default)]
72 skipped: usize,
73 reason: String,
75 },
76 #[serde(alias = "Completed")]
78 Completed {
79 processed: usize,
81 #[serde(default)]
83 skipped: usize,
84 duration_secs: f64,
86 },
87 #[serde(alias = "Failed")]
89 Failed {
90 processed: usize,
92 total: usize,
94 #[serde(default)]
96 skipped: usize,
97 error: String,
99 },
100 #[serde(alias = "Cancelled")]
102 Cancelled {
103 processed: usize,
105 total: usize,
107 #[serde(default)]
109 skipped: usize,
110 },
111}
112
113impl MigrationState {
114 #[inline]
116 pub fn is_resumable(&self) -> bool {
117 matches!(
118 self,
119 MigrationState::Paused { .. } | MigrationState::Failed { .. }
120 )
121 }
122
123 #[inline]
125 pub fn is_terminal(&self) -> bool {
126 matches!(
127 self,
128 MigrationState::Completed { .. } | MigrationState::Cancelled { .. }
129 )
130 }
131
132 #[inline]
134 pub fn is_active(&self) -> bool {
135 matches!(self, MigrationState::InProgress { .. })
136 }
137
138 pub fn progress(&self) -> Option<f64> {
140 match self {
141 MigrationState::Planned => Some(0.0),
142 MigrationState::InProgress {
143 processed, total, ..
144 }
145 | MigrationState::Paused {
146 processed, total, ..
147 }
148 | MigrationState::Failed {
149 processed, total, ..
150 }
151 | MigrationState::Cancelled {
152 processed, total, ..
153 } => {
154 if *total == 0 {
155 Some(1.0)
156 } else {
157 Some(*processed as f64 / *total as f64)
158 }
159 }
160 MigrationState::Completed { .. } => Some(1.0),
161 }
162 }
163
164 pub fn processed(&self) -> usize {
166 match self {
167 MigrationState::Planned => 0,
168 MigrationState::InProgress { processed, .. }
169 | MigrationState::Paused { processed, .. }
170 | MigrationState::Failed { processed, .. }
171 | MigrationState::Cancelled { processed, .. }
172 | MigrationState::Completed { processed, .. } => *processed,
173 }
174 }
175
176 pub fn skipped(&self) -> usize {
178 match self {
179 MigrationState::Planned => 0,
180 MigrationState::InProgress { skipped, .. }
181 | MigrationState::Paused { skipped, .. }
182 | MigrationState::Failed { skipped, .. }
183 | MigrationState::Cancelled { skipped, .. }
184 | MigrationState::Completed { skipped, .. } => *skipped,
185 }
186 }
187
188 pub fn total(&self) -> usize {
190 match self {
191 MigrationState::Planned | MigrationState::Completed { .. } => 0,
192 MigrationState::InProgress { total, .. }
193 | MigrationState::Paused { total, .. }
194 | MigrationState::Failed { total, .. }
195 | MigrationState::Cancelled { total, .. } => *total,
196 }
197 }
198
199 pub fn effective_total(&self) -> usize {
201 self.total().saturating_sub(self.skipped())
202 }
203
204 pub fn effective_coverage(&self) -> f64 {
206 let eff = self.effective_total();
207 if eff == 0 {
208 1.0
209 } else {
210 self.processed() as f64 / eff as f64
211 }
212 }
213}
214
215#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct MigrationPlan {
236 pub id: String,
238 pub source_model: EmbeddingModel,
240 pub target_model: EmbeddingModel,
242 pub total_embeddings: usize,
244 pub batch_size: usize,
246 pub created_at: String,
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct MigrationProgress {
253 pub migration_id: String,
255 pub state: MigrationState,
257 #[serde(default)]
259 pub skipped: usize,
260 #[serde(default)]
262 pub effective_total: usize,
263 #[serde(default)]
265 pub effective_coverage: f64,
266 pub throughput: f64,
268 pub eta_secs: Option<f64>,
270 pub error_count: usize,
272}
273
274#[derive(Debug, Clone)]
276#[non_exhaustive]
277pub enum MigrationError {
278 InvalidTransition {
280 from: String,
282 to: String,
284 },
285}
286
287impl std::fmt::Display for MigrationError {
288 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
289 match self {
290 MigrationError::InvalidTransition { from, to } => {
291 write!(f, "invalid migration transition from {from} to {to}")
292 }
293 }
294 }
295}
296
297impl std::error::Error for MigrationError {}