1use std::collections::HashMap;
29use std::sync::atomic::{AtomicU64, Ordering};
30use std::time::{Duration, Instant};
31
32use crate::pool::{Connection, QueryRows};
33use crate::value::Value;
34
35#[derive(Debug, Clone)]
37pub struct ShadowComparison {
38 pub sql: String,
40 pub orm_duration: Duration,
42 pub raw_duration: Duration,
44 pub orm_rows: usize,
46 pub raw_rows: usize,
48 pub consistent: bool,
50 pub mismatch: Option<String>,
52}
53
54impl ShadowComparison {
55 pub fn latency_ratio(&self) -> f64 {
57 if self.raw_duration.is_zero() {
58 1.0
59 } else {
60 self.orm_duration.as_secs_f64() / self.raw_duration.as_secs_f64()
61 }
62 }
63}
64
65#[derive(Debug, Default)]
67pub struct ShadowStats {
68 pub comparisons: AtomicU64,
70 pub mismatches: AtomicU64,
72 pub orm_total_us: AtomicU64,
74 pub raw_total_us: AtomicU64,
76}
77
78impl ShadowStats {
79 pub fn avg_orm_us(&self) -> u64 {
81 let n = self.comparisons.load(Ordering::Relaxed);
82 self.orm_total_us
84 .load(Ordering::Relaxed)
85 .checked_div(n)
86 .unwrap_or(0)
87 }
88
89 pub fn avg_raw_us(&self) -> u64 {
91 let n = self.comparisons.load(Ordering::Relaxed);
92 self.raw_total_us
94 .load(Ordering::Relaxed)
95 .checked_div(n)
96 .unwrap_or(0)
97 }
98
99 pub fn mismatch_rate(&self) -> f64 {
101 let n = self.comparisons.load(Ordering::Relaxed);
102 if n == 0 {
103 0.0
104 } else {
105 self.mismatches.load(Ordering::Relaxed) as f64 / n as f64
106 }
107 }
108}
109
110#[derive(Debug, Clone)]
112pub struct ShadowConfig {
113 pub timeout: Duration,
115 pub row_count_only: bool,
117 pub max_compare_rows: usize,
119}
120
121impl Default for ShadowConfig {
122 fn default() -> Self {
123 Self {
124 timeout: Duration::from_secs(3),
125 row_count_only: false,
126 max_compare_rows: 10_000,
127 }
128 }
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub enum MismatchAction {
134 Record,
136 Error,
138 Panic,
140}
141
142pub struct ShadowConnection<C: Connection> {
151 orm_conn: C,
153 raw_conn: C,
155 config: ShadowConfig,
157 on_mismatch: MismatchAction,
159 stats: ShadowStats,
161}
162
163impl<C: Connection> ShadowConnection<C> {
164 pub fn new(orm_conn: C, raw_conn: C, config: ShadowConfig) -> Self {
166 Self {
167 orm_conn,
168 raw_conn,
169 config,
170 on_mismatch: MismatchAction::Record,
171 stats: ShadowStats::default(),
172 }
173 }
174
175 pub fn with_mismatch_action(mut self, action: MismatchAction) -> Self {
177 self.on_mismatch = action;
178 self
179 }
180
181 pub fn stats(&self) -> &ShadowStats {
183 &self.stats
184 }
185
186 pub async fn query_shadow(&mut self, sql: &str) -> Result<QueryRows, crate::DbError> {
190 let orm_fut = self.orm_conn.query(sql);
192 let raw_fut = self.raw_conn.query(sql);
193
194 let orm_start = Instant::now();
195 let orm_result = tokio::time::timeout(self.config.timeout, orm_fut).await;
196 let orm_duration = orm_start.elapsed();
197
198 let raw_start = Instant::now();
199 let raw_result = tokio::time::timeout(self.config.timeout, raw_fut).await;
200 let raw_duration = raw_start.elapsed();
201
202 self.stats
204 .orm_total_us
205 .fetch_add(orm_duration.as_micros() as u64, Ordering::Relaxed);
206 self.stats
207 .raw_total_us
208 .fetch_add(raw_duration.as_micros() as u64, Ordering::Relaxed);
209 self.stats.comparisons.fetch_add(1, Ordering::Relaxed);
210
211 let orm_rows = match orm_result {
213 Ok(Ok(rows)) => rows,
214 Ok(Err(e)) => {
215 self.stats.mismatches.fetch_add(1, Ordering::Relaxed);
216 self.handle_mismatch(ShadowComparison {
217 sql: sql.to_string(),
218 orm_duration,
219 raw_duration,
220 orm_rows: 0,
221 raw_rows: 0,
222 consistent: false,
223 mismatch: Some(format!("ORM error: {}", e)),
224 });
225 return Err(e);
226 }
227 Err(_) => {
228 self.stats.mismatches.fetch_add(1, Ordering::Relaxed);
229 return Err(crate::DbError::ConnectionTimeout(format!(
230 "ORM shadow path timeout after {:?}",
231 self.config.timeout
232 )));
233 }
234 };
235
236 let raw_rows = match raw_result {
237 Ok(Ok(rows)) => rows,
238 Ok(Err(e)) => {
239 self.stats.mismatches.fetch_add(1, Ordering::Relaxed);
240 self.handle_mismatch(ShadowComparison {
241 sql: sql.to_string(),
242 orm_duration,
243 raw_duration,
244 orm_rows: orm_rows.len(),
245 raw_rows: 0,
246 consistent: false,
247 mismatch: Some(format!("Raw path error: {}", e)),
248 });
249 return Ok(orm_rows);
251 }
252 Err(_) => {
253 self.stats.mismatches.fetch_add(1, Ordering::Relaxed);
254 self.handle_mismatch(ShadowComparison {
255 sql: sql.to_string(),
256 orm_duration,
257 raw_duration,
258 orm_rows: orm_rows.len(),
259 raw_rows: 0,
260 consistent: false,
261 mismatch: Some(format!("Raw path timeout after {:?}", self.config.timeout)),
262 });
263 return Ok(orm_rows);
264 }
265 };
266
267 let comparison = self.compare_rows(sql, orm_duration, raw_duration, &orm_rows, &raw_rows);
269 if !comparison.consistent {
270 self.stats.mismatches.fetch_add(1, Ordering::Relaxed);
271 self.handle_mismatch(comparison);
272 }
273
274 Ok(orm_rows)
275 }
276
277 fn compare_rows(
279 &self,
280 sql: &str,
281 orm_duration: Duration,
282 raw_duration: Duration,
283 orm_rows: &[HashMap<String, Value>],
284 raw_rows: &[HashMap<String, Value>],
285 ) -> ShadowComparison {
286 if orm_rows.len() != raw_rows.len() {
288 return ShadowComparison {
289 sql: sql.to_string(),
290 orm_duration,
291 raw_duration,
292 orm_rows: orm_rows.len(),
293 raw_rows: raw_rows.len(),
294 consistent: false,
295 mismatch: Some(format!(
296 "Row count mismatch: ORM={} vs Raw={}",
297 orm_rows.len(),
298 raw_rows.len()
299 )),
300 };
301 }
302
303 if self.config.row_count_only {
305 return ShadowComparison {
306 sql: sql.to_string(),
307 orm_duration,
308 raw_duration,
309 orm_rows: orm_rows.len(),
310 raw_rows: raw_rows.len(),
311 consistent: true,
312 mismatch: None,
313 };
314 }
315
316 let limit = self.config.max_compare_rows.min(orm_rows.len());
318 for (i, (orm_row, raw_row)) in orm_rows[..limit]
319 .iter()
320 .zip(raw_rows[..limit].iter())
321 .enumerate()
322 {
323 if orm_row.len() != raw_row.len() {
324 return ShadowComparison {
325 sql: sql.to_string(),
326 orm_duration,
327 raw_duration,
328 orm_rows: orm_rows.len(),
329 raw_rows: raw_rows.len(),
330 consistent: false,
331 mismatch: Some(format!(
332 "Row {} column count mismatch: ORM={} vs Raw={}",
333 i,
334 orm_row.len(),
335 raw_row.len()
336 )),
337 };
338 }
339 for (key, orm_val) in orm_row {
340 match raw_row.get(key) {
341 Some(raw_val) if !values_equal(orm_val, raw_val) => {
342 return ShadowComparison {
343 sql: sql.to_string(),
344 orm_duration,
345 raw_duration,
346 orm_rows: orm_rows.len(),
347 raw_rows: raw_rows.len(),
348 consistent: false,
349 mismatch: Some(format!(
350 "Row {} column '{}' value mismatch: ORM={:?} vs Raw={:?}",
351 i, key, orm_val, raw_val
352 )),
353 };
354 }
355 None => {
356 return ShadowComparison {
357 sql: sql.to_string(),
358 orm_duration,
359 raw_duration,
360 orm_rows: orm_rows.len(),
361 raw_rows: raw_rows.len(),
362 consistent: false,
363 mismatch: Some(format!(
364 "Row {} column '{}' missing in raw result",
365 i, key
366 )),
367 };
368 }
369 _ => {}
370 }
371 }
372 }
373
374 ShadowComparison {
375 sql: sql.to_string(),
376 orm_duration,
377 raw_duration,
378 orm_rows: orm_rows.len(),
379 raw_rows: raw_rows.len(),
380 consistent: true,
381 mismatch: None,
382 }
383 }
384
385 fn handle_mismatch(&self, comparison: ShadowComparison) {
387 tracing::warn!(
388 target: "sz_orm::shadow",
389 sql = %comparison.sql,
390 orm_rows = comparison.orm_rows,
391 raw_rows = comparison.raw_rows,
392 orm_us = ?comparison.orm_duration,
393 raw_us = ?comparison.raw_duration,
394 mismatch = ?comparison.mismatch,
395 "Shadow traffic mismatch detected"
396 );
397
398 match self.on_mismatch {
399 MismatchAction::Record => { }
400 MismatchAction::Error => { }
401 MismatchAction::Panic => {
402 panic!(
403 "Shadow traffic mismatch: SQL={:?} mismatch={:?}",
404 comparison.sql, comparison.mismatch
405 );
406 }
407 }
408 }
409}
410
411fn values_equal(a: &Value, b: &Value) -> bool {
413 use Value::*;
414 match (a, b) {
415 (Null, Null) => true,
416 (Bool(x), Bool(y)) => x == y,
417 (I8(x), I8(y)) => x == y,
418 (I8(x), I16(y)) => (*x as i16) == *y,
419 (I16(x), I8(y)) => *x == (*y as i16),
420 (I16(x), I16(y)) => x == y,
421 (I16(x), I32(y)) => (*x as i32) == *y,
422 (I32(x), I16(y)) => *x == (*y as i32),
423 (I32(x), I32(y)) => x == y,
424 (I32(x), I64(y)) => (*x as i64) == *y,
425 (I64(x), I32(y)) => *x == (*y as i64),
426 (I64(x), I64(y)) => x == y,
427 (U8(x), U8(y)) => x == y,
428 (U16(x), U16(y)) => x == y,
429 (U32(x), U32(y)) => x == y,
430 (U64(x), U64(y)) => x == y,
431 (F32(x), F32(y)) => (x - y).abs() < 1e-6,
432 (F32(x), F64(y)) => ((*x as f64) - y).abs() < 1e-6,
433 (F64(x), F32(y)) => (x - (*y as f64)).abs() < 1e-6,
434 (F64(x), F64(y)) => (x - y).abs() < 1e-9,
435 (String(x), String(y)) => x == y,
436 (Bytes(x), Bytes(y)) => x == y,
437 _ => a == b,
438 }
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444
445 #[test]
446 fn test_values_equal_numeric_cross_type() {
447 assert!(values_equal(&Value::I32(42), &Value::I64(42)));
448 assert!(values_equal(&Value::I8(1), &Value::I16(1)));
449 assert!(!values_equal(&Value::I32(42), &Value::I64(43)));
450 }
451
452 #[test]
453 fn test_values_equal_float_tolerance() {
454 assert!(values_equal(&Value::F32(1.0), &Value::F64(1.0)));
455 assert!(!values_equal(&Value::F32(1.0), &Value::F64(2.0)));
456 }
457
458 #[test]
459 fn test_shadow_stats_mismatch_rate() {
460 let stats = ShadowStats::default();
461 assert_eq!(stats.mismatch_rate(), 0.0);
462 stats.comparisons.store(10, Ordering::Relaxed);
463 stats.mismatches.store(1, Ordering::Relaxed);
464 assert!((stats.mismatch_rate() - 0.1).abs() < 1e-9);
465 }
466}