1use serde::{Deserialize, Serialize};
9
10use crate::model::EmbeddingModel;
11
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
14#[serde(rename_all = "snake_case")]
15#[non_exhaustive]
16pub enum SkipReason {
17 ContentTooLarge {
19 size: usize,
21 max: usize,
23 },
24 InvalidEncoding(String),
26 ContentDeleted,
28 PermanentApiError(String),
30 ManualSkip(String),
32}
33
34impl std::fmt::Display for SkipReason {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 match self {
37 SkipReason::ContentTooLarge { size, max } => {
38 write!(f, "content too large: {size} bytes (max {max})")
39 }
40 SkipReason::InvalidEncoding(enc) => write!(f, "invalid encoding: {enc}"),
41 SkipReason::ContentDeleted => write!(f, "content deleted"),
42 SkipReason::PermanentApiError(msg) => write!(f, "permanent API error: {msg}"),
43 SkipReason::ManualSkip(reason) => write!(f, "manually skipped: {reason}"),
44 }
45 }
46}
47
48#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
50#[serde(rename_all = "snake_case")]
51#[non_exhaustive]
52pub enum MigrationState {
53 #[serde(alias = "Planned")]
56 Planned,
57 #[serde(alias = "InProgress")]
59 InProgress {
60 processed: usize,
62 total: usize,
64 #[serde(default)]
66 skipped: usize,
67 },
68 #[serde(alias = "Paused")]
70 Paused {
71 processed: usize,
73 total: usize,
75 #[serde(default)]
77 skipped: usize,
78 reason: String,
80 },
81 #[serde(alias = "Completed")]
83 Completed {
84 processed: usize,
86 #[serde(default)]
88 skipped: usize,
89 duration_secs: f64,
91 },
92 #[serde(alias = "Failed")]
94 Failed {
95 processed: usize,
97 total: usize,
99 #[serde(default)]
101 skipped: usize,
102 error: String,
104 },
105 #[serde(alias = "Cancelled")]
107 Cancelled {
108 processed: usize,
110 total: usize,
112 #[serde(default)]
114 skipped: usize,
115 },
116}
117
118impl MigrationState {
119 #[inline]
121 pub fn is_resumable(&self) -> bool {
122 matches!(
123 self,
124 MigrationState::Paused { .. } | MigrationState::Failed { .. }
125 )
126 }
127
128 #[inline]
130 pub fn is_terminal(&self) -> bool {
131 matches!(
132 self,
133 MigrationState::Completed { .. } | MigrationState::Cancelled { .. }
134 )
135 }
136
137 #[inline]
139 pub fn is_active(&self) -> bool {
140 matches!(self, MigrationState::InProgress { .. })
141 }
142
143 pub fn progress(&self) -> Option<f64> {
145 match self {
146 MigrationState::Planned => Some(0.0),
147 MigrationState::InProgress {
148 processed, total, ..
149 }
150 | MigrationState::Paused {
151 processed, total, ..
152 }
153 | MigrationState::Failed {
154 processed, total, ..
155 }
156 | MigrationState::Cancelled {
157 processed, total, ..
158 } => {
159 if *total == 0 {
160 Some(1.0)
161 } else {
162 Some(*processed as f64 / *total as f64)
163 }
164 }
165 MigrationState::Completed { .. } => Some(1.0),
166 }
167 }
168
169 pub fn processed(&self) -> usize {
171 match self {
172 MigrationState::Planned => 0,
173 MigrationState::InProgress { processed, .. }
174 | MigrationState::Paused { processed, .. }
175 | MigrationState::Failed { processed, .. }
176 | MigrationState::Cancelled { processed, .. }
177 | MigrationState::Completed { processed, .. } => *processed,
178 }
179 }
180
181 pub fn skipped(&self) -> usize {
183 match self {
184 MigrationState::Planned => 0,
185 MigrationState::InProgress { skipped, .. }
186 | MigrationState::Paused { skipped, .. }
187 | MigrationState::Failed { skipped, .. }
188 | MigrationState::Cancelled { skipped, .. }
189 | MigrationState::Completed { skipped, .. } => *skipped,
190 }
191 }
192
193 pub fn total(&self) -> usize {
195 match self {
196 MigrationState::Planned | MigrationState::Completed { .. } => 0,
197 MigrationState::InProgress { total, .. }
198 | MigrationState::Paused { total, .. }
199 | MigrationState::Failed { total, .. }
200 | MigrationState::Cancelled { total, .. } => *total,
201 }
202 }
203
204 pub fn effective_total(&self) -> usize {
206 self.total().saturating_sub(self.skipped())
207 }
208
209 pub fn effective_coverage(&self) -> f64 {
211 let eff = self.effective_total();
212 if eff == 0 {
213 1.0
214 } else {
215 self.processed() as f64 / eff as f64
216 }
217 }
218}
219
220#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct MigrationPlan {
225 pub id: String,
227 pub source_model: EmbeddingModel,
229 pub target_model: EmbeddingModel,
231 pub total_embeddings: usize,
233 pub batch_size: usize,
235 pub created_at: String,
237}
238
239#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct MigrationProgress {
242 pub migration_id: String,
244 pub state: MigrationState,
246 #[serde(default)]
248 pub skipped: usize,
249 #[serde(default)]
251 pub effective_total: usize,
252 #[serde(default)]
254 pub effective_coverage: f64,
255 pub throughput: f64,
257 pub eta_secs: Option<f64>,
259 pub error_count: usize,
261}
262
263#[derive(Debug, Clone)]
265#[non_exhaustive]
266pub enum MigrationError {
267 InvalidTransition {
269 from: String,
271 to: String,
273 },
274}
275
276impl std::fmt::Display for MigrationError {
277 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
278 match self {
279 MigrationError::InvalidTransition { from, to } => {
280 write!(f, "invalid migration transition from {from} to {to}")
281 }
282 }
283 }
284}
285
286impl std::error::Error for MigrationError {}