1use crate::egress::decoder::{DecodedBatch, ZstdScratch, decode_result_batch};
30use crate::egress::schema::Schema;
31use crate::egress::symbol_dict::SymbolDict;
32use crate::egress::wire::ByteReader;
33use crate::egress::wire::cache_reset::resets_dict;
34use crate::egress::wire::capabilities::has_zone;
35use crate::egress::wire::header::FrameHeader;
36use crate::egress::wire::msg_kind::{MsgKind, StatusCode};
37use crate::egress::wire::roles;
38use crate::error::{Result, fmt};
39use bytes::Bytes;
40
41#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
51#[non_exhaustive]
52pub enum ServerRole {
53 Standalone,
54 Primary,
55 Replica,
56 PrimaryCatchup,
57 Other(u8),
59}
60
61impl ServerRole {
62 pub fn from_u8(byte: u8) -> Self {
63 match byte {
64 roles::STANDALONE => ServerRole::Standalone,
65 roles::PRIMARY => ServerRole::Primary,
66 roles::REPLICA => ServerRole::Replica,
67 roles::PRIMARY_CATCHUP => ServerRole::PrimaryCatchup,
68 other => ServerRole::Other(other),
69 }
70 }
71
72 pub fn as_str(self) -> String {
77 match self {
78 ServerRole::Standalone => roles::NAME_STANDALONE.to_string(),
79 ServerRole::Primary => roles::NAME_PRIMARY.to_string(),
80 ServerRole::Replica => roles::NAME_REPLICA.to_string(),
81 ServerRole::PrimaryCatchup => roles::NAME_PRIMARY_CATCHUP.to_string(),
82 ServerRole::Other(b) => format!("UNKNOWN({})", b),
83 }
84 }
85
86 pub fn as_u8(self) -> u8 {
89 match self {
90 ServerRole::Standalone => roles::STANDALONE,
91 ServerRole::Primary => roles::PRIMARY,
92 ServerRole::Replica => roles::REPLICA,
93 ServerRole::PrimaryCatchup => roles::PRIMARY_CATCHUP,
94 ServerRole::Other(b) => b,
95 }
96 }
97}
98
99#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct ServerInfo {
102 pub role: ServerRole,
103 pub epoch: u64,
104 pub capabilities: u32,
105 pub server_wall_ns: i64,
106 pub cluster_id: String,
107 pub node_id: String,
108 pub zone_id: Option<String>,
114}
115
116#[derive(Debug, Clone, PartialEq, Eq)]
144#[non_exhaustive]
145pub struct UpgradeReject {
146 pub role_byte: u8,
147 pub role_name: String,
148 pub zone: Option<String>,
149}
150
151impl UpgradeReject {
152 pub fn new(role_byte: u8, role_name: impl Into<String>, zone: Option<String>) -> Self {
153 Self {
154 role_byte,
155 role_name: role_name.into(),
156 zone,
157 }
158 }
159
160 pub fn is_transient(&self) -> bool {
168 self.role_byte == roles::PRIMARY_CATCHUP
169 || self
170 .role_name
171 .eq_ignore_ascii_case(roles::NAME_PRIMARY_CATCHUP)
172 }
173}
174
175#[derive(Debug, Clone)]
181pub enum ServerEvent {
182 Batch(DecodedBatch),
184 End {
186 request_id: i64,
187 final_seq: u64,
188 total_rows: u64,
189 },
190 Error {
192 request_id: i64,
193 status: StatusCode,
194 message: String,
195 },
196 ExecDone {
198 request_id: i64,
199 op_type: u8,
200 rows_affected: u64,
201 },
202 CacheReset {
204 #[allow(dead_code)]
210 mask: u8,
211 },
212 ServerInfo(ServerInfo),
214}
215
216pub fn decode_frame(
226 header: FrameHeader,
227 payload: &Bytes,
228 dict: &mut SymbolDict,
229 query_schema: &mut Option<Schema>,
230 zstd_scratch: &mut ZstdScratch,
231) -> Result<ServerEvent> {
232 if payload.is_empty() {
233 return Err(fmt!(ProtocolError, "frame payload is empty"));
234 }
235 let kind_byte = payload[0];
236 let kind = MsgKind::from_u8(kind_byte)?;
237 let expected_tc = if matches!(kind, MsgKind::ResultBatch) {
242 1
243 } else {
244 0
245 };
246 if header.table_count != expected_tc {
247 return Err(fmt!(
248 ProtocolError,
249 "frame for msg_kind 0x{:02X} has table_count {} (expected {})",
250 kind_byte,
251 header.table_count,
252 expected_tc
253 ));
254 }
255 match kind {
256 MsgKind::ResultBatch => Ok(ServerEvent::Batch(decode_result_batch(
257 payload,
258 header.flags,
259 dict,
260 query_schema,
261 zstd_scratch,
262 )?)),
263 MsgKind::ResultEnd => decode_result_end(payload),
264 MsgKind::QueryError => decode_query_error(payload),
265 MsgKind::ExecDone => decode_exec_done(payload),
266 MsgKind::CacheReset => decode_cache_reset(payload, dict),
267 MsgKind::ServerInfo => decode_server_info(payload),
268 MsgKind::QueryRequest | MsgKind::Cancel | MsgKind::Credit => Err(fmt!(
270 ProtocolError,
271 "server sent client-only message kind 0x{:02X}",
272 kind_byte
273 )),
274 }
275}
276
277fn decode_result_end(payload: &[u8]) -> Result<ServerEvent> {
282 let mut r = ByteReader::new(payload);
283 expect_kind(&mut r, MsgKind::ResultEnd)?;
284 let request_id = r.read_i64_le()?;
285 let final_seq = r.read_varint_u64()?;
286 let total_rows = r.read_varint_u64()?;
287 expect_eof(&r, "RESULT_END")?;
288 Ok(ServerEvent::End {
289 request_id,
290 final_seq,
291 total_rows,
292 })
293}
294
295fn decode_query_error(payload: &[u8]) -> Result<ServerEvent> {
296 let mut r = ByteReader::new(payload);
297 expect_kind(&mut r, MsgKind::QueryError)?;
298 let request_id = r.read_i64_le()?;
299 let status = StatusCode::from_u8(r.read_u8()?)?;
300 let msg_len = r.read_u16_le()? as usize;
301 let bytes = r.read_bytes(msg_len)?;
302 let message = std::str::from_utf8(bytes)
303 .map_err(|e| fmt!(InvalidUtf8, "QUERY_ERROR message not valid UTF-8: {}", e))?
304 .to_string();
305 expect_eof(&r, "QUERY_ERROR")?;
306 Ok(ServerEvent::Error {
307 request_id,
308 status,
309 message,
310 })
311}
312
313fn decode_exec_done(payload: &[u8]) -> Result<ServerEvent> {
314 let mut r = ByteReader::new(payload);
315 expect_kind(&mut r, MsgKind::ExecDone)?;
316 let request_id = r.read_i64_le()?;
317 let op_type = r.read_u8()?;
318 let rows_affected = r.read_varint_u64()?;
319 expect_eof(&r, "EXEC_DONE")?;
320 Ok(ServerEvent::ExecDone {
321 request_id,
322 op_type,
323 rows_affected,
324 })
325}
326
327fn decode_cache_reset(payload: &[u8], dict: &mut SymbolDict) -> Result<ServerEvent> {
328 let mut r = ByteReader::new(payload);
329 expect_kind(&mut r, MsgKind::CacheReset)?;
330 let mask = r.read_u8()?;
331 expect_eof(&r, "CACHE_RESET")?;
332 if resets_dict(mask) {
338 dict.reset();
339 }
340 Ok(ServerEvent::CacheReset { mask })
341}
342
343fn decode_server_info(payload: &[u8]) -> Result<ServerEvent> {
344 let mut r = ByteReader::new(payload);
345 expect_kind(&mut r, MsgKind::ServerInfo)?;
346 let role = ServerRole::from_u8(r.read_u8()?);
347 let epoch = r.read_u64_le()?;
348 let capabilities = r.read_u32_le()?;
349 let server_wall_ns = r.read_i64_le()?;
350 let cluster_id = read_u16_string(&mut r, "cluster_id")?;
351 let node_id = read_u16_string(&mut r, "node_id")?;
352 let zone_id = if has_zone(capabilities) {
361 Some(read_u16_string(&mut r, "zone_id")?)
362 } else {
363 None
364 };
365 expect_eof(&r, "SERVER_INFO")?;
366 Ok(ServerEvent::ServerInfo(ServerInfo {
367 role,
368 epoch,
369 capabilities,
370 server_wall_ns,
371 cluster_id,
372 node_id,
373 zone_id,
374 }))
375}
376
377fn expect_kind(r: &mut ByteReader<'_>, expected: MsgKind) -> Result<()> {
382 let got = r.read_u8()?;
383 if got != expected.as_u8() {
384 return Err(fmt!(
385 ProtocolError,
386 "expected msg_kind 0x{:02X}, got 0x{:02X}",
387 expected.as_u8(),
388 got
389 ));
390 }
391 Ok(())
392}
393
394fn expect_eof(r: &ByteReader<'_>, msg_name: &str) -> Result<()> {
395 if !r.is_empty() {
396 return Err(fmt!(
397 ProtocolError,
398 "{} has {} trailing bytes",
399 msg_name,
400 r.remaining().len()
401 ));
402 }
403 Ok(())
404}
405
406fn read_u16_string(r: &mut ByteReader<'_>, field: &str) -> Result<String> {
407 let len = r.read_u16_le()? as usize;
408 let bytes = r.read_bytes(len)?;
409 std::str::from_utf8(bytes)
410 .map_err(|e| fmt!(InvalidUtf8, "{} not valid UTF-8: {}", field, e))
411 .map(|s| s.to_string())
412}
413
414#[cfg(test)]
419mod tests {
420 use super::*;
421 use crate::egress::wire::header::{HEADER_LEN, PROTOCOL_VERSION};
422 use crate::egress::wire::varint::encode_u64;
423 use crate::error::ErrorCode;
424
425 fn header(payload_len: usize) -> FrameHeader {
426 FrameHeader {
427 version: PROTOCOL_VERSION,
428 flags: 0,
429 table_count: 0,
430 payload_length: payload_len as u32,
431 }
432 }
433
434 fn build_result_end(rid: i64, final_seq: u64, total_rows: u64) -> Bytes {
437 let mut p = vec![MsgKind::ResultEnd.as_u8()];
438 p.extend_from_slice(&rid.to_le_bytes());
439 encode_u64(final_seq, &mut p);
440 encode_u64(total_rows, &mut p);
441 Bytes::from(p)
442 }
443
444 #[test]
445 fn decode_result_end_ok() {
446 let payload = build_result_end(42, 7, 1_000);
447 let mut dict = SymbolDict::new();
448 let mut schema: Option<Schema> = None;
449 let event = decode_frame(
450 header(payload.len()),
451 &payload,
452 &mut dict,
453 &mut schema,
454 &mut ZstdScratch::new(),
455 )
456 .unwrap();
457 match event {
458 ServerEvent::End {
459 request_id,
460 final_seq,
461 total_rows,
462 } => {
463 assert_eq!(request_id, 42);
464 assert_eq!(final_seq, 7);
465 assert_eq!(total_rows, 1000);
466 }
467 _ => panic!("wrong event"),
468 }
469 }
470
471 fn build_query_error(rid: i64, status: StatusCode, msg: &str) -> Bytes {
474 let mut p = vec![MsgKind::QueryError.as_u8()];
475 p.extend_from_slice(&rid.to_le_bytes());
476 p.push(status.as_u8());
477 p.extend_from_slice(&(msg.len() as u16).to_le_bytes());
478 p.extend_from_slice(msg.as_bytes());
479 Bytes::from(p)
480 }
481
482 #[test]
483 fn decode_query_error_ok() {
484 let payload = build_query_error(9, StatusCode::ParseError, "bad SQL");
485 let mut dict = SymbolDict::new();
486 let mut schema: Option<Schema> = None;
487 let event = decode_frame(
488 header(payload.len()),
489 &payload,
490 &mut dict,
491 &mut schema,
492 &mut ZstdScratch::new(),
493 )
494 .unwrap();
495 match event {
496 ServerEvent::Error {
497 request_id,
498 status,
499 message,
500 } => {
501 assert_eq!(request_id, 9);
502 assert_eq!(status, StatusCode::ParseError);
503 assert_eq!(message, "bad SQL");
504 }
505 _ => panic!("wrong event"),
506 }
507 }
508
509 #[test]
510 fn query_error_truncated_message_rejected() {
511 let payload = build_query_error(1, StatusCode::InternalError, "details");
512 let truncated = payload.slice(..payload.len() - 3);
513 let mut dict = SymbolDict::new();
514 let mut schema: Option<Schema> = None;
515 let err = decode_frame(
516 header(truncated.len()),
517 &truncated,
518 &mut dict,
519 &mut schema,
520 &mut ZstdScratch::new(),
521 )
522 .unwrap_err();
523 assert_eq!(err.code(), ErrorCode::ProtocolError);
524 }
525
526 #[test]
527 fn query_error_invalid_utf8_rejected() {
528 let mut p = vec![MsgKind::QueryError.as_u8()];
529 p.extend_from_slice(&1i64.to_le_bytes());
530 p.push(StatusCode::InternalError.as_u8());
531 p.extend_from_slice(&2u16.to_le_bytes());
532 p.extend_from_slice(&[0xFF, 0xFE]);
533 let p = Bytes::from(p);
534 let mut dict = SymbolDict::new();
535 let mut schema: Option<Schema> = None;
536 let err = decode_frame(
537 header(p.len()),
538 &p,
539 &mut dict,
540 &mut schema,
541 &mut ZstdScratch::new(),
542 )
543 .unwrap_err();
544 assert_eq!(err.code(), ErrorCode::InvalidUtf8);
545 }
546
547 #[test]
550 fn decode_exec_done_ok() {
551 let mut p = vec![MsgKind::ExecDone.as_u8()];
552 p.extend_from_slice(&5i64.to_le_bytes());
553 p.push(0xAB); encode_u64(0, &mut p); let p = Bytes::from(p);
556 let mut dict = SymbolDict::new();
557 let mut schema: Option<Schema> = None;
558 let event = decode_frame(
559 header(p.len()),
560 &p,
561 &mut dict,
562 &mut schema,
563 &mut ZstdScratch::new(),
564 )
565 .unwrap();
566 match event {
567 ServerEvent::ExecDone {
568 request_id,
569 op_type,
570 rows_affected,
571 } => {
572 assert_eq!(request_id, 5);
573 assert_eq!(op_type, 0xAB);
574 assert_eq!(rows_affected, 0);
575 }
576 _ => panic!("wrong event"),
577 }
578 }
579
580 fn build_cache_reset(mask: u8) -> Bytes {
583 Bytes::from(vec![MsgKind::CacheReset.as_u8(), mask])
584 }
585
586 #[test]
587 fn cache_reset_clears_dict() {
588 let mut dict = SymbolDict::new();
589 dict.apply_delta(0, [b"x".as_slice()]).unwrap();
590 let mut query_schema: Option<Schema> = None;
591
592 let payload = build_cache_reset(0x01);
593 let event = decode_frame(
594 header(payload.len()),
595 &payload,
596 &mut dict,
597 &mut query_schema,
598 &mut ZstdScratch::new(),
599 )
600 .unwrap();
601 assert!(matches!(event, ServerEvent::CacheReset { mask: 0x01 }));
602 assert_eq!(dict.len(), 0);
603 }
604
605 #[test]
606 fn cache_reset_ignores_reserved_bits() {
607 let mut dict = SymbolDict::new();
613 dict.apply_delta(0, [b"x".as_slice()]).unwrap();
614 let mut query_schema: Option<Schema> = None;
615
616 let payload = build_cache_reset(0x83);
618 let event = decode_frame(
619 header(payload.len()),
620 &payload,
621 &mut dict,
622 &mut query_schema,
623 &mut ZstdScratch::new(),
624 )
625 .unwrap();
626 assert!(matches!(event, ServerEvent::CacheReset { mask: 0x83 }));
627 assert_eq!(
628 dict.len(),
629 0,
630 "DICT bit must apply even with reserved bits set"
631 );
632 }
633
634 fn build_server_info(role: u8, cluster: &str, node: &str) -> Bytes {
637 build_server_info_with(role, 0, cluster, node, None)
638 }
639
640 fn build_server_info_with(
643 role: u8,
644 capabilities: u32,
645 cluster: &str,
646 node: &str,
647 zone: Option<&str>,
648 ) -> Bytes {
649 let mut p = vec![MsgKind::ServerInfo.as_u8()];
650 p.push(role);
651 p.extend_from_slice(&7u64.to_le_bytes()); p.extend_from_slice(&capabilities.to_le_bytes());
653 p.extend_from_slice(&123_456_789i64.to_le_bytes()); p.extend_from_slice(&(cluster.len() as u16).to_le_bytes());
655 p.extend_from_slice(cluster.as_bytes());
656 p.extend_from_slice(&(node.len() as u16).to_le_bytes());
657 p.extend_from_slice(node.as_bytes());
658 if let Some(z) = zone {
659 p.extend_from_slice(&(z.len() as u16).to_le_bytes());
660 p.extend_from_slice(z.as_bytes());
661 }
662 Bytes::from(p)
663 }
664
665 #[test]
666 fn decode_server_info_primary() {
667 let payload = build_server_info(0x01, "cluster-A", "node-1");
668 let mut dict = SymbolDict::new();
669 let mut schema: Option<Schema> = None;
670 let event = decode_frame(
671 header(payload.len()),
672 &payload,
673 &mut dict,
674 &mut schema,
675 &mut ZstdScratch::new(),
676 )
677 .unwrap();
678 let ServerEvent::ServerInfo(info) = event else {
679 panic!()
680 };
681 assert_eq!(info.role, ServerRole::Primary);
682 assert_eq!(info.epoch, 7);
683 assert_eq!(info.capabilities, 0);
684 assert_eq!(info.server_wall_ns, 123_456_789);
685 assert_eq!(info.cluster_id, "cluster-A");
686 assert_eq!(info.node_id, "node-1");
687 assert_eq!(info.zone_id, None, "CAP_ZONE=0 leaves zone_id absent");
688 }
689
690 #[test]
691 fn unknown_role_byte_is_other_variant() {
692 let payload = build_server_info(0x55, "c", "n");
693 let mut dict = SymbolDict::new();
694 let mut schema: Option<Schema> = None;
695 let event = decode_frame(
696 header(payload.len()),
697 &payload,
698 &mut dict,
699 &mut schema,
700 &mut ZstdScratch::new(),
701 )
702 .unwrap();
703 let ServerEvent::ServerInfo(info) = event else {
704 panic!()
705 };
706 assert_eq!(info.role, ServerRole::Other(0x55));
707 }
708
709 #[test]
710 fn decode_server_info_with_cap_zone_reads_zone_id() {
711 let payload = build_server_info_with(
713 0x01,
714 crate::egress::wire::CAP_ZONE,
715 "cluster-A",
716 "node-1",
717 Some("eu-west-1a"),
718 );
719 let mut dict = SymbolDict::new();
720 let mut schema: Option<Schema> = None;
721 let event = decode_frame(
722 header(payload.len()),
723 &payload,
724 &mut dict,
725 &mut schema,
726 &mut ZstdScratch::new(),
727 )
728 .unwrap();
729 let ServerEvent::ServerInfo(info) = event else {
730 panic!()
731 };
732 assert_eq!(
733 info.capabilities & crate::egress::wire::CAP_ZONE,
734 crate::egress::wire::CAP_ZONE
735 );
736 assert_eq!(info.zone_id.as_deref(), Some("eu-west-1a"));
737 }
738
739 #[test]
740 fn cap_zone_set_but_zone_id_missing_is_protocol_error() {
741 let payload = build_server_info_with(
746 0x01,
747 crate::egress::wire::CAP_ZONE,
748 "c",
749 "n",
750 None, );
752 let mut dict = SymbolDict::new();
753 let mut schema: Option<Schema> = None;
754 let err = decode_frame(
755 header(payload.len()),
756 &payload,
757 &mut dict,
758 &mut schema,
759 &mut ZstdScratch::new(),
760 )
761 .unwrap_err();
762 assert_eq!(err.code(), ErrorCode::ProtocolError);
763 }
764
765 #[test]
766 fn unknown_capabilities_bit_with_trailing_bytes_is_protocol_error() {
767 let mut payload = build_server_info(0x01, "c", "n").to_vec();
773 payload.extend_from_slice(&[0xDE, 0xAD]);
776 let payload = Bytes::from(payload);
777 let mut dict = SymbolDict::new();
778 let mut schema: Option<Schema> = None;
779 let err = decode_frame(
780 header(payload.len()),
781 &payload,
782 &mut dict,
783 &mut schema,
784 &mut ZstdScratch::new(),
785 )
786 .unwrap_err();
787 assert_eq!(err.code(), ErrorCode::ProtocolError);
788 }
789
790 #[test]
793 fn empty_payload_rejected() {
794 let mut dict = SymbolDict::new();
795 let mut schema: Option<Schema> = None;
796 let empty = Bytes::new();
797 let err = decode_frame(
798 header(0),
799 &empty,
800 &mut dict,
801 &mut schema,
802 &mut ZstdScratch::new(),
803 )
804 .unwrap_err();
805 assert_eq!(err.code(), ErrorCode::ProtocolError);
806 }
807
808 #[test]
809 fn unknown_msg_kind_rejected() {
810 let mut dict = SymbolDict::new();
811 let mut schema: Option<Schema> = None;
812 let p = Bytes::from(vec![0xAA]);
813 let err = decode_frame(
814 header(1),
815 &p,
816 &mut dict,
817 &mut schema,
818 &mut ZstdScratch::new(),
819 )
820 .unwrap_err();
821 assert_eq!(err.code(), ErrorCode::ProtocolError);
822 }
823
824 #[test]
825 fn client_only_kinds_rejected_from_server() {
826 for k in [
827 MsgKind::QueryRequest.as_u8(),
828 MsgKind::Cancel.as_u8(),
829 MsgKind::Credit.as_u8(),
830 ] {
831 let mut dict = SymbolDict::new();
832 let mut schema: Option<Schema> = None;
833 let p = Bytes::from(vec![k]);
834 let err = decode_frame(
835 header(1),
836 &p,
837 &mut dict,
838 &mut schema,
839 &mut ZstdScratch::new(),
840 )
841 .unwrap_err();
842 assert_eq!(err.code(), ErrorCode::ProtocolError);
843 assert!(err.msg().contains("client-only"));
844 }
845 }
846
847 #[test]
848 fn trailing_bytes_rejected_for_simple_messages() {
849 let payload = build_result_end(1, 0, 0);
850 let mut bytes_vec: Vec<u8> = payload.to_vec();
851 bytes_vec.push(0xFF);
852 let payload = Bytes::from(bytes_vec);
853 let mut dict = SymbolDict::new();
854 let mut schema: Option<Schema> = None;
855 let err = decode_frame(
856 header(payload.len()),
857 &payload,
858 &mut dict,
859 &mut schema,
860 &mut ZstdScratch::new(),
861 )
862 .unwrap_err();
863 assert_eq!(err.code(), ErrorCode::ProtocolError);
864 }
865
866 #[test]
868 fn header_len_is_12() {
869 assert_eq!(HEADER_LEN, 12);
870 }
871
872 #[test]
873 fn upgrade_reject_round_trips() {
874 let r = UpgradeReject::new(
875 roles::PRIMARY_CATCHUP,
876 roles::NAME_PRIMARY_CATCHUP,
877 Some("eu-west-1a".into()),
878 );
879 assert_eq!(r.role_byte, roles::PRIMARY_CATCHUP);
880 assert_eq!(r.zone.as_deref(), Some("eu-west-1a"));
881 assert!(r.is_transient());
882 }
883
884 #[test]
885 fn upgrade_reject_topological_for_non_catchup_roles() {
886 for (byte, name) in [
889 (roles::STANDALONE, roles::NAME_STANDALONE),
890 (roles::PRIMARY, roles::NAME_PRIMARY),
891 (roles::REPLICA, roles::NAME_REPLICA),
892 (0x99, "FUTURE_ROLE"),
893 ] {
894 let r = UpgradeReject::new(byte, name, None);
895 assert!(!r.is_transient(), "role {} should be topological", name);
896 }
897 }
898
899 #[test]
900 fn upgrade_reject_is_transient_case_insensitive() {
901 let r = UpgradeReject::new(0x99, "primary_catchup", None);
902 assert!(r.is_transient(), "case-insensitive match per spec §5");
903 }
904}