1use byteorder::{ByteOrder, LittleEndian};
13use serde::Serialize;
14
15const MYSQL_TYPE_TINY: u8 = 1;
21const MYSQL_TYPE_SHORT: u8 = 2;
23const MYSQL_TYPE_LONG: u8 = 3;
25const MYSQL_TYPE_FLOAT: u8 = 4;
27const MYSQL_TYPE_DOUBLE: u8 = 5;
29const MYSQL_TYPE_LONGLONG: u8 = 8;
31const MYSQL_TYPE_INT24: u8 = 9;
33const MYSQL_TYPE_DATETIME: u8 = 12;
35const MYSQL_TYPE_VARCHAR: u8 = 15;
37const MYSQL_TYPE_BIT: u8 = 16;
39const MYSQL_TYPE_TIMESTAMP2: u8 = 17;
41const MYSQL_TYPE_DATETIME2: u8 = 18;
43const MYSQL_TYPE_TIME2: u8 = 19;
45const MYSQL_TYPE_NEWDECIMAL: u8 = 246;
47const MYSQL_TYPE_ENUM: u8 = 247;
49const MYSQL_TYPE_SET: u8 = 248;
51const MYSQL_TYPE_BLOB: u8 = 252;
53const MYSQL_TYPE_VAR_STRING: u8 = 253;
55const MYSQL_TYPE_STRING: u8 = 254;
57
58#[derive(Debug, Clone, Serialize)]
64pub struct BinlogColumnMeta {
65 pub column_type: u8,
67 pub is_unsigned: bool,
69 pub type_metadata: u16,
71 pub is_pk: bool,
73 pub pk_ordinal: Option<usize>,
75}
76
77#[derive(Debug, Clone, PartialEq, Serialize)]
79pub enum BinlogPkValue {
80 Int(i64),
82 Uint(u64),
84 Str(String),
86 Bytes(Vec<u8>),
88}
89
90impl std::fmt::Display for BinlogPkValue {
91 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 match self {
93 BinlogPkValue::Int(v) => write!(f, "{}", v),
94 BinlogPkValue::Uint(v) => write!(f, "{}", v),
95 BinlogPkValue::Str(s) => write!(f, "{}", s),
96 BinlogPkValue::Bytes(b) => {
97 write!(f, "0x")?;
98 for byte in b {
99 write!(f, "{:02x}", byte)?;
100 }
101 Ok(())
102 }
103 }
104 }
105}
106
107pub fn parse_column_metadata(column_types: &[u8], metadata_bytes: &[u8]) -> Vec<u16> {
126 let mut result = Vec::with_capacity(column_types.len());
127 let mut offset = 0;
128
129 for &col_type in column_types {
130 let (value, consumed) = read_type_metadata(col_type, metadata_bytes, offset);
131 result.push(value);
132 offset += consumed;
133 }
134
135 result
136}
137
138fn read_type_metadata(column_type: u8, data: &[u8], offset: usize) -> (u16, usize) {
142 match column_type {
143 MYSQL_TYPE_TINY | MYSQL_TYPE_SHORT | MYSQL_TYPE_LONG | MYSQL_TYPE_LONGLONG
145 | MYSQL_TYPE_INT24 | MYSQL_TYPE_DATETIME => (0, 0),
146
147 MYSQL_TYPE_FLOAT | MYSQL_TYPE_DOUBLE => {
149 let v = data.get(offset).copied().unwrap_or(0) as u16;
150 (v, 1)
151 }
152 MYSQL_TYPE_DATETIME2 | MYSQL_TYPE_TIMESTAMP2 | MYSQL_TYPE_TIME2 => {
153 let v = data.get(offset).copied().unwrap_or(0) as u16;
154 (v, 1)
155 }
156 MYSQL_TYPE_BLOB => {
157 let v = data.get(offset).copied().unwrap_or(0) as u16;
159 (v, 1)
160 }
161
162 MYSQL_TYPE_VARCHAR | MYSQL_TYPE_VAR_STRING => {
164 if offset + 2 <= data.len() {
165 (LittleEndian::read_u16(&data[offset..]), 2)
166 } else {
167 (0, 2)
168 }
169 }
170 MYSQL_TYPE_NEWDECIMAL => {
171 let precision = data.get(offset).copied().unwrap_or(0) as u16;
173 let scale = data.get(offset + 1).copied().unwrap_or(0) as u16;
174 ((precision << 8) | scale, 2)
175 }
176 MYSQL_TYPE_STRING | MYSQL_TYPE_ENUM | MYSQL_TYPE_SET => {
177 if offset + 2 <= data.len() {
179 let b0 = data[offset] as u16;
180 let b1 = data[offset + 1] as u16;
181 ((b0 << 8) | b1, 2)
182 } else {
183 (0, 2)
184 }
185 }
186 MYSQL_TYPE_BIT => {
187 if offset + 2 <= data.len() {
189 let bits = data[offset] as u16;
190 let bytes = data[offset + 1] as u16;
191 ((bytes << 8) | bits, 2)
192 } else {
193 (0, 2)
194 }
195 }
196
197 _ => (0, 0),
199 }
200}
201
202pub fn extract_pk_from_row_image(
222 row_data: &[u8],
223 columns: &[BinlogColumnMeta],
224) -> Option<Vec<BinlogPkValue>> {
225 let col_count = columns.len();
226 if col_count == 0 {
227 return None;
228 }
229
230 let null_bitmap_len = col_count.div_ceil(8);
232 if row_data.len() < null_bitmap_len {
233 return None;
234 }
235 let null_bitmap = &row_data[..null_bitmap_len];
236 let mut offset = null_bitmap_len;
237
238 let pk_count = columns.iter().filter(|c| c.is_pk).count();
240 if pk_count == 0 {
241 return None;
242 }
243 let mut pk_values: Vec<(usize, BinlogPkValue)> = Vec::with_capacity(pk_count);
244
245 for (i, col) in columns.iter().enumerate() {
246 let is_null = (null_bitmap[i / 8] >> (i % 8)) & 1 == 1;
248
249 if is_null {
250 if col.is_pk {
251 return None;
253 }
254 continue;
255 }
256
257 let remaining = &row_data[offset..];
259 let size = match column_value_size(col.column_type, col.type_metadata, remaining) {
260 Some(s) => s,
261 None => {
262 if col.is_pk {
263 return None;
264 }
265 return None;
267 }
268 };
269
270 if col.is_pk {
271 if remaining.len() < size {
272 return None;
273 }
274 let value = decode_column_value(
275 col.column_type,
276 col.is_unsigned,
277 col.type_metadata,
278 &remaining[..size],
279 )?;
280 let ordinal = col.pk_ordinal.unwrap_or(0);
281 pk_values.push((ordinal, value));
282 }
283
284 offset += size;
285 }
286
287 pk_values.sort_by_key(|(ord, _)| *ord);
289 Some(pk_values.into_iter().map(|(_, v)| v).collect())
290}
291
292fn column_value_size(column_type: u8, metadata: u16, data: &[u8]) -> Option<usize> {
301 match column_type {
302 MYSQL_TYPE_TINY => Some(1),
303 MYSQL_TYPE_SHORT => Some(2),
304 MYSQL_TYPE_INT24 => Some(3),
305 MYSQL_TYPE_LONG => Some(4),
306 MYSQL_TYPE_LONGLONG => Some(8),
307 MYSQL_TYPE_FLOAT => Some(4),
308 MYSQL_TYPE_DOUBLE => Some(8),
309 MYSQL_TYPE_DATETIME => Some(8), MYSQL_TYPE_VARCHAR | MYSQL_TYPE_VAR_STRING => {
312 let max_len = metadata as usize;
314 if max_len < 256 {
315 let len = *data.first()? as usize;
316 Some(1 + len)
317 } else {
318 if data.len() < 2 {
319 return None;
320 }
321 let len = LittleEndian::read_u16(data) as usize;
322 Some(2 + len)
323 }
324 }
325
326 MYSQL_TYPE_STRING => {
327 let real_type = (metadata >> 8) as u8;
331 let max_len = metadata & 0xFF;
332
333 match real_type {
334 MYSQL_TYPE_ENUM => {
336 let size = max_len as usize;
337 Some(size)
338 }
339 MYSQL_TYPE_SET => {
341 let size = max_len as usize;
342 Some(size)
343 }
344 _ => {
346 if max_len > 255 {
347 if data.len() < 2 {
348 return None;
349 }
350 let len = LittleEndian::read_u16(data) as usize;
351 Some(2 + len)
352 } else {
353 let len = *data.first()? as usize;
354 Some(1 + len)
355 }
356 }
357 }
358 }
359
360 MYSQL_TYPE_BLOB => {
361 let pack_len = metadata as usize;
363 if pack_len == 0 || data.len() < pack_len {
364 return None;
365 }
366 let blob_len = match pack_len {
367 1 => data[0] as usize,
368 2 => LittleEndian::read_u16(data) as usize,
369 3 => data[0] as usize | (data[1] as usize) << 8 | (data[2] as usize) << 16,
370 4 => LittleEndian::read_u32(data) as usize,
371 _ => return None,
372 };
373 Some(pack_len + blob_len)
374 }
375
376 MYSQL_TYPE_NEWDECIMAL => {
377 let precision = (metadata >> 8) as usize;
378 let scale = (metadata & 0xFF) as usize;
379 Some(decimal_binary_size(precision, scale))
380 }
381
382 MYSQL_TYPE_BIT => {
383 let bits = (metadata & 0xFF) as usize;
384 let bytes = (metadata >> 8) as usize;
385 Some(bytes + if bits > 0 { 1 } else { 0 })
386 }
387
388 MYSQL_TYPE_DATETIME2 => {
389 let fsp = metadata as usize;
391 Some(5 + fsp_storage_size(fsp))
392 }
393
394 MYSQL_TYPE_TIMESTAMP2 => {
395 let fsp = metadata as usize;
397 Some(4 + fsp_storage_size(fsp))
398 }
399
400 MYSQL_TYPE_TIME2 => {
401 let fsp = metadata as usize;
403 Some(3 + fsp_storage_size(fsp))
404 }
405
406 _ => None,
408 }
409}
410
411fn fsp_storage_size(fsp: usize) -> usize {
415 fsp.div_ceil(2)
416}
417
418fn decimal_binary_size(precision: usize, scale: usize) -> usize {
422 const DIG2BYTES: [usize; 10] = [0, 1, 1, 2, 2, 3, 3, 4, 4, 4];
424
425 let intg = precision - scale;
426 let intg_full = intg / 9;
427 let intg_leftover = intg % 9;
428 let frac_full = scale / 9;
429 let frac_leftover = scale % 9;
430
431 intg_full * 4 + DIG2BYTES[intg_leftover] + frac_full * 4 + DIG2BYTES[frac_leftover]
432}
433
434fn decode_column_value(
443 column_type: u8,
444 is_unsigned: bool,
445 metadata: u16,
446 data: &[u8],
447) -> Option<BinlogPkValue> {
448 match column_type {
449 MYSQL_TYPE_TINY => {
450 if data.is_empty() {
451 return None;
452 }
453 if is_unsigned {
454 Some(BinlogPkValue::Uint(data[0] as u64))
455 } else {
456 Some(BinlogPkValue::Int(data[0] as i8 as i64))
457 }
458 }
459
460 MYSQL_TYPE_SHORT => {
461 if data.len() < 2 {
462 return None;
463 }
464 if is_unsigned {
465 Some(BinlogPkValue::Uint(LittleEndian::read_u16(data) as u64))
466 } else {
467 Some(BinlogPkValue::Int(LittleEndian::read_i16(data) as i64))
468 }
469 }
470
471 MYSQL_TYPE_INT24 => {
472 if data.len() < 3 {
473 return None;
474 }
475 let raw = data[0] as u32 | (data[1] as u32) << 8 | (data[2] as u32) << 16;
476 if is_unsigned {
477 Some(BinlogPkValue::Uint(raw as u64))
478 } else {
479 let signed = if raw & 0x800000 != 0 {
481 (raw | 0xFF000000) as i32 as i64
482 } else {
483 raw as i64
484 };
485 Some(BinlogPkValue::Int(signed))
486 }
487 }
488
489 MYSQL_TYPE_LONG => {
490 if data.len() < 4 {
491 return None;
492 }
493 if is_unsigned {
494 Some(BinlogPkValue::Uint(LittleEndian::read_u32(data) as u64))
495 } else {
496 Some(BinlogPkValue::Int(LittleEndian::read_i32(data) as i64))
497 }
498 }
499
500 MYSQL_TYPE_LONGLONG => {
501 if data.len() < 8 {
502 return None;
503 }
504 if is_unsigned {
505 Some(BinlogPkValue::Uint(LittleEndian::read_u64(data)))
506 } else {
507 Some(BinlogPkValue::Int(LittleEndian::read_i64(data)))
508 }
509 }
510
511 MYSQL_TYPE_FLOAT => {
512 if data.len() < 4 {
513 return None;
514 }
515 let bits = LittleEndian::read_u32(data);
517 let val = f32::from_bits(bits);
518 Some(BinlogPkValue::Str(format!("{}", val)))
519 }
520
521 MYSQL_TYPE_DOUBLE => {
522 if data.len() < 8 {
523 return None;
524 }
525 let bits = LittleEndian::read_u64(data);
526 let val = f64::from_bits(bits);
527 Some(BinlogPkValue::Str(format!("{}", val)))
528 }
529
530 MYSQL_TYPE_VARCHAR | MYSQL_TYPE_VAR_STRING => {
531 let max_len = metadata as usize;
532 if max_len < 256 {
533 let len = *data.first()? as usize;
534 if data.len() < 1 + len {
535 return None;
536 }
537 let s = String::from_utf8_lossy(&data[1..1 + len]).into_owned();
538 Some(BinlogPkValue::Str(s))
539 } else {
540 if data.len() < 2 {
541 return None;
542 }
543 let len = LittleEndian::read_u16(data) as usize;
544 if data.len() < 2 + len {
545 return None;
546 }
547 let s = String::from_utf8_lossy(&data[2..2 + len]).into_owned();
548 Some(BinlogPkValue::Str(s))
549 }
550 }
551
552 MYSQL_TYPE_STRING => {
553 let real_type = (metadata >> 8) as u8;
554 let max_len = metadata & 0xFF;
555
556 match real_type {
557 MYSQL_TYPE_ENUM => {
558 let size = max_len as usize;
559 if data.len() < size {
560 return None;
561 }
562 let val = match size {
563 1 => data[0] as u64,
564 2 => LittleEndian::read_u16(data) as u64,
565 _ => return None,
566 };
567 Some(BinlogPkValue::Uint(val))
568 }
569 MYSQL_TYPE_SET => {
570 let size = max_len as usize;
571 if data.len() < size {
572 return None;
573 }
574 Some(BinlogPkValue::Bytes(data[..size].to_vec()))
575 }
576 _ => {
577 if max_len > 255 {
579 if data.len() < 2 {
580 return None;
581 }
582 let len = LittleEndian::read_u16(data) as usize;
583 if data.len() < 2 + len {
584 return None;
585 }
586 let s = String::from_utf8_lossy(&data[2..2 + len]).into_owned();
587 Some(BinlogPkValue::Str(s))
588 } else {
589 let len = *data.first()? as usize;
590 if data.len() < 1 + len {
591 return None;
592 }
593 let s = String::from_utf8_lossy(&data[1..1 + len]).into_owned();
594 Some(BinlogPkValue::Str(s))
595 }
596 }
597 }
598 }
599
600 MYSQL_TYPE_BLOB => {
601 let pack_len = metadata as usize;
602 if pack_len == 0 || data.len() < pack_len {
603 return None;
604 }
605 let blob_len = match pack_len {
606 1 => data[0] as usize,
607 2 => LittleEndian::read_u16(data) as usize,
608 3 => data[0] as usize | (data[1] as usize) << 8 | (data[2] as usize) << 16,
609 4 => LittleEndian::read_u32(data) as usize,
610 _ => return None,
611 };
612 if data.len() < pack_len + blob_len {
613 return None;
614 }
615 Some(BinlogPkValue::Bytes(
616 data[pack_len..pack_len + blob_len].to_vec(),
617 ))
618 }
619
620 _ => None,
622 }
623}
624
625#[cfg(test)]
630mod tests {
631 use super::*;
632
633 #[test]
636 fn parse_metadata_int_types() {
637 let types = vec![MYSQL_TYPE_TINY, MYSQL_TYPE_LONG, MYSQL_TYPE_LONGLONG];
639 let meta = parse_column_metadata(&types, &[]);
640 assert_eq!(meta, vec![0, 0, 0]);
641 }
642
643 #[test]
644 fn parse_metadata_varchar() {
645 let types = vec![MYSQL_TYPE_VARCHAR];
646 let meta_bytes = [0xC8, 0x00];
648 let meta = parse_column_metadata(&types, &meta_bytes);
649 assert_eq!(meta, vec![200]);
650 }
651
652 #[test]
653 fn parse_metadata_blob() {
654 let types = vec![MYSQL_TYPE_BLOB];
655 let meta_bytes = [2]; let meta = parse_column_metadata(&types, &meta_bytes);
657 assert_eq!(meta, vec![2]);
658 }
659
660 #[test]
661 fn parse_metadata_mixed() {
662 let types = vec![
664 MYSQL_TYPE_LONG,
665 MYSQL_TYPE_VARCHAR,
666 MYSQL_TYPE_BLOB,
667 MYSQL_TYPE_SHORT,
668 ];
669 let meta_bytes = [0x00, 0x01, 3]; let meta = parse_column_metadata(&types, &meta_bytes);
671 assert_eq!(meta, vec![0, 256, 3, 0]);
672 }
673
674 #[test]
675 fn parse_metadata_newdecimal() {
676 let types = vec![MYSQL_TYPE_NEWDECIMAL];
677 let meta_bytes = [10, 2];
679 let meta = parse_column_metadata(&types, &meta_bytes);
680 assert_eq!(meta, vec![2562]);
682 }
683
684 #[test]
685 fn parse_metadata_datetime2() {
686 let types = vec![MYSQL_TYPE_DATETIME2];
687 let meta_bytes = [3]; let meta = parse_column_metadata(&types, &meta_bytes);
689 assert_eq!(meta, vec![3]);
690 }
691
692 #[test]
695 fn decode_tiny_signed() {
696 let cols = vec![make_pk_col(MYSQL_TYPE_TINY, false, 0, 0)];
697 let mut data = vec![0u8; 1]; data.push(0xFE); let pk = extract_pk_from_row_image(&data, &cols).unwrap();
700 assert_eq!(pk, vec![BinlogPkValue::Int(-2)]);
701 }
702
703 #[test]
704 fn decode_tiny_unsigned() {
705 let cols = vec![make_pk_col(MYSQL_TYPE_TINY, true, 0, 0)];
706 let mut data = vec![0u8; 1];
707 data.push(0xFE); let pk = extract_pk_from_row_image(&data, &cols).unwrap();
709 assert_eq!(pk, vec![BinlogPkValue::Uint(254)]);
710 }
711
712 #[test]
713 fn decode_long_signed() {
714 let cols = vec![make_pk_col(MYSQL_TYPE_LONG, false, 0, 0)];
715 let mut data = vec![0u8; 1];
716 let mut buf = [0u8; 4];
717 LittleEndian::write_i32(&mut buf, -42);
718 data.extend_from_slice(&buf);
719 let pk = extract_pk_from_row_image(&data, &cols).unwrap();
720 assert_eq!(pk, vec![BinlogPkValue::Int(-42)]);
721 }
722
723 #[test]
724 fn decode_long_unsigned() {
725 let cols = vec![make_pk_col(MYSQL_TYPE_LONG, true, 0, 0)];
726 let mut data = vec![0u8; 1];
727 let mut buf = [0u8; 4];
728 LittleEndian::write_u32(&mut buf, 3_000_000_000);
729 data.extend_from_slice(&buf);
730 let pk = extract_pk_from_row_image(&data, &cols).unwrap();
731 assert_eq!(pk, vec![BinlogPkValue::Uint(3_000_000_000)]);
732 }
733
734 #[test]
735 fn decode_longlong_signed() {
736 let cols = vec![make_pk_col(MYSQL_TYPE_LONGLONG, false, 0, 0)];
737 let mut data = vec![0u8; 1];
738 let mut buf = [0u8; 8];
739 LittleEndian::write_i64(&mut buf, -9_000_000_000);
740 data.extend_from_slice(&buf);
741 let pk = extract_pk_from_row_image(&data, &cols).unwrap();
742 assert_eq!(pk, vec![BinlogPkValue::Int(-9_000_000_000)]);
743 }
744
745 #[test]
746 fn decode_longlong_unsigned() {
747 let cols = vec![make_pk_col(MYSQL_TYPE_LONGLONG, true, 0, 0)];
748 let mut data = vec![0u8; 1];
749 let mut buf = [0u8; 8];
750 LittleEndian::write_u64(&mut buf, 18_000_000_000_000_000_000);
751 data.extend_from_slice(&buf);
752 let pk = extract_pk_from_row_image(&data, &cols).unwrap();
753 assert_eq!(pk, vec![BinlogPkValue::Uint(18_000_000_000_000_000_000)]);
754 }
755
756 #[test]
757 fn decode_short_signed() {
758 let cols = vec![make_pk_col(MYSQL_TYPE_SHORT, false, 0, 0)];
759 let mut data = vec![0u8; 1];
760 let mut buf = [0u8; 2];
761 LittleEndian::write_i16(&mut buf, -1000);
762 data.extend_from_slice(&buf);
763 let pk = extract_pk_from_row_image(&data, &cols).unwrap();
764 assert_eq!(pk, vec![BinlogPkValue::Int(-1000)]);
765 }
766
767 #[test]
768 fn decode_int24_signed() {
769 let cols = vec![make_pk_col(MYSQL_TYPE_INT24, false, 0, 0)];
770 let mut data = vec![0u8; 1];
771 data.extend_from_slice(&[0x9C, 0xFF, 0xFF]);
773 let pk = extract_pk_from_row_image(&data, &cols).unwrap();
774 assert_eq!(pk, vec![BinlogPkValue::Int(-100)]);
775 }
776
777 #[test]
778 fn decode_int24_unsigned() {
779 let cols = vec![make_pk_col(MYSQL_TYPE_INT24, true, 0, 0)];
780 let mut data = vec![0u8; 1];
781 data.extend_from_slice(&[0xA0, 0x86, 0x01]);
783 let pk = extract_pk_from_row_image(&data, &cols).unwrap();
784 assert_eq!(pk, vec![BinlogPkValue::Uint(100_000)]);
785 }
786
787 #[test]
790 fn decode_varchar_short_prefix() {
791 let cols = vec![make_pk_col(MYSQL_TYPE_VARCHAR, false, 200, 0)];
793 let mut data = vec![0u8; 1]; data.push(5); data.extend_from_slice(b"hello");
796 let pk = extract_pk_from_row_image(&data, &cols).unwrap();
797 assert_eq!(pk, vec![BinlogPkValue::Str("hello".to_string())]);
798 }
799
800 #[test]
801 fn decode_varchar_long_prefix() {
802 let cols = vec![make_pk_col(MYSQL_TYPE_VARCHAR, false, 500, 0)];
804 let mut data = vec![0u8; 1]; let mut len_buf = [0u8; 2];
806 LittleEndian::write_u16(&mut len_buf, 11);
807 data.extend_from_slice(&len_buf);
808 data.extend_from_slice(b"hello world");
809 let pk = extract_pk_from_row_image(&data, &cols).unwrap();
810 assert_eq!(pk, vec![BinlogPkValue::Str("hello world".to_string())]);
811 }
812
813 #[test]
816 fn null_pk_returns_none() {
817 let cols = vec![make_pk_col(MYSQL_TYPE_LONG, false, 0, 0)];
818 let data = vec![0x01];
820 assert!(extract_pk_from_row_image(&data, &cols).is_none());
821 }
822
823 #[test]
824 fn null_non_pk_column_skipped() {
825 let cols = vec![
827 BinlogColumnMeta {
828 column_type: MYSQL_TYPE_LONG,
829 is_unsigned: true,
830 type_metadata: 0,
831 is_pk: false,
832 pk_ordinal: None,
833 },
834 make_pk_col(MYSQL_TYPE_LONG, true, 0, 0),
835 ];
836 let mut data = vec![0x01]; let mut buf = [0u8; 4];
838 LittleEndian::write_u32(&mut buf, 42);
839 data.extend_from_slice(&buf);
840 let pk = extract_pk_from_row_image(&data, &cols).unwrap();
841 assert_eq!(pk, vec![BinlogPkValue::Uint(42)]);
842 }
843
844 #[test]
847 fn multi_column_pk_sorted_by_ordinal() {
848 let cols = vec![
850 BinlogColumnMeta {
851 column_type: MYSQL_TYPE_SHORT,
852 is_unsigned: true,
853 type_metadata: 0,
854 is_pk: true,
855 pk_ordinal: Some(1),
856 },
857 BinlogColumnMeta {
858 column_type: MYSQL_TYPE_LONG,
859 is_unsigned: true,
860 type_metadata: 0,
861 is_pk: false,
862 pk_ordinal: None,
863 },
864 BinlogColumnMeta {
865 column_type: MYSQL_TYPE_TINY,
866 is_unsigned: true,
867 type_metadata: 0,
868 is_pk: true,
869 pk_ordinal: Some(0),
870 },
871 ];
872 let mut data = vec![0u8; 1]; let mut buf = [0u8; 2];
875 LittleEndian::write_u16(&mut buf, 1000);
876 data.extend_from_slice(&buf);
877 let mut buf4 = [0u8; 4];
879 LittleEndian::write_u32(&mut buf4, 99999);
880 data.extend_from_slice(&buf4);
881 data.push(7);
883
884 let pk = extract_pk_from_row_image(&data, &cols).unwrap();
885 assert_eq!(pk, vec![BinlogPkValue::Uint(7), BinlogPkValue::Uint(1000)]);
887 }
888
889 #[test]
892 fn size_varchar_short() {
893 let data = [5, b'h', b'e', b'l', b'l', b'o'];
894 assert_eq!(column_value_size(MYSQL_TYPE_VARCHAR, 200, &data), Some(6));
895 }
896
897 #[test]
898 fn size_varchar_long() {
899 let mut data = vec![0u8; 2];
900 LittleEndian::write_u16(&mut data, 3);
901 data.extend_from_slice(b"abc");
902 assert_eq!(column_value_size(MYSQL_TYPE_VARCHAR, 500, &data), Some(5));
903 }
904
905 #[test]
906 fn size_blob_pack2() {
907 let mut data = vec![0u8; 2];
908 LittleEndian::write_u16(&mut data, 10);
909 data.extend_from_slice(&[0u8; 10]);
910 assert_eq!(column_value_size(MYSQL_TYPE_BLOB, 2, &data), Some(12));
911 }
912
913 #[test]
914 fn size_datetime2_fsp3() {
915 assert_eq!(
917 column_value_size(MYSQL_TYPE_DATETIME2, 3, &[0u8; 10]),
918 Some(7)
919 );
920 }
921
922 #[test]
925 fn pk_value_display() {
926 assert_eq!(BinlogPkValue::Int(-42).to_string(), "-42");
927 assert_eq!(BinlogPkValue::Uint(100).to_string(), "100");
928 assert_eq!(BinlogPkValue::Str("hello".into()).to_string(), "hello");
929 assert_eq!(BinlogPkValue::Bytes(vec![0xDE, 0xAD]).to_string(), "0xdead");
930 }
931
932 #[test]
935 fn decimal_size_10_2() {
936 assert_eq!(decimal_binary_size(10, 2), 5);
938 }
939
940 #[test]
941 fn decimal_size_18_0() {
942 assert_eq!(decimal_binary_size(18, 0), 8);
944 }
945
946 #[test]
949 fn empty_columns_returns_none() {
950 assert!(extract_pk_from_row_image(&[0], &[]).is_none());
951 }
952
953 #[test]
954 fn no_pk_columns_returns_none() {
955 let cols = vec![BinlogColumnMeta {
956 column_type: MYSQL_TYPE_LONG,
957 is_unsigned: true,
958 type_metadata: 0,
959 is_pk: false,
960 pk_ordinal: None,
961 }];
962 let mut data = vec![0u8; 1];
963 data.extend_from_slice(&[0u8; 4]);
964 assert!(extract_pk_from_row_image(&data, &cols).is_none());
965 }
966
967 #[test]
968 fn truncated_data_returns_none() {
969 let cols = vec![make_pk_col(MYSQL_TYPE_LONGLONG, false, 0, 0)];
970 let data = vec![0u8; 1]; assert!(extract_pk_from_row_image(&data, &cols).is_none());
972 }
973
974 fn make_pk_col(col_type: u8, unsigned: bool, meta: u16, ordinal: usize) -> BinlogColumnMeta {
977 BinlogColumnMeta {
978 column_type: col_type,
979 is_unsigned: unsigned,
980 type_metadata: meta,
981 is_pk: true,
982 pk_ordinal: Some(ordinal),
983 }
984 }
985}