1use super::lsn::Lsn;
13use super::types::PgColumn;
14use bytes::Bytes;
15
16const PG_EPOCH_OFFSET_US: i64 = 946_684_800_000_000;
19const MAX_POSTGRES_COLUMNS: usize = 1_600;
20
21#[derive(Debug, Clone, PartialEq)]
23pub enum WalMessage {
24 Begin(BeginMessage),
26 Commit(CommitMessage),
28 Relation(RelationMessage),
30 Insert(InsertMessage),
32 Update(UpdateMessage),
34 Delete(DeleteMessage),
36 Truncate(TruncateMessage),
38 Origin(OriginMessage),
40 Type(TypeMessage),
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct BeginMessage {
47 pub final_lsn: Lsn,
49 pub commit_ts_ms: i64,
51 pub xid: u32,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct CommitMessage {
58 pub flags: u8,
60 pub commit_lsn: Lsn,
62 pub end_lsn: Lsn,
64 pub commit_ts_ms: i64,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct RelationMessage {
71 pub relation_id: u32,
73 pub namespace: String,
75 pub name: String,
77 pub replica_identity: u8,
79 pub columns: Vec<PgColumn>,
81}
82
83#[derive(Debug, Clone, PartialEq)]
85pub struct InsertMessage {
86 pub relation_id: u32,
88 pub new_tuple: TupleData,
90}
91
92#[derive(Debug, Clone, PartialEq)]
94pub enum OldTuple {
95 Key(TupleData),
97 Full(TupleData),
99}
100
101#[derive(Debug, Clone, PartialEq)]
103pub struct UpdateMessage {
104 pub relation_id: u32,
106 pub old_tuple: Option<OldTuple>,
108 pub new_tuple: TupleData,
110}
111
112#[derive(Debug, Clone, PartialEq)]
114pub struct DeleteMessage {
115 pub relation_id: u32,
117 pub old_tuple: OldTuple,
119}
120
121#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct TruncateMessage {
124 pub relation_ids: Vec<u32>,
126 pub options: u8,
128}
129
130#[derive(Debug, Clone, PartialEq, Eq)]
132pub struct OriginMessage {
133 pub origin_lsn: Lsn,
135 pub name: String,
137}
138
139#[derive(Debug, Clone, PartialEq, Eq)]
141pub struct TypeMessage {
142 pub type_id: u32,
144 pub namespace: String,
146 pub name: String,
148}
149
150#[derive(Debug, Clone, PartialEq)]
152pub struct TupleData {
153 pub columns: Vec<ColumnValue>,
155}
156
157#[derive(Debug, Clone, PartialEq)]
159pub enum ColumnValue {
160 Null,
162 Unchanged,
164 Text(Bytes),
166}
167
168impl ColumnValue {
169 #[must_use]
171 pub fn as_text(&self) -> Option<&str> {
172 match self {
173 ColumnValue::Text(bytes) => std::str::from_utf8(bytes).ok(),
174 _ => None,
175 }
176 }
177
178 #[must_use]
180 pub fn is_null(&self) -> bool {
181 matches!(self, ColumnValue::Null)
182 }
183}
184
185#[derive(Debug, Clone, thiserror::Error)]
187pub enum DecoderError {
188 #[error("unexpected end of data at offset {offset}, need {needed} bytes")]
190 UnexpectedEof {
191 offset: usize,
193 needed: usize,
195 },
196
197 #[error("unknown message type: 0x{0:02X}")]
199 UnknownMessageType(u8),
200
201 #[error("invalid data: {0}")]
203 InvalidData(String),
204
205 #[error("invalid UTF-8 at offset {0}")]
207 InvalidUtf8(usize),
208}
209
210struct Cursor {
212 data: Bytes,
213 pos: usize,
214}
215
216impl Cursor {
217 fn new(data: Bytes) -> Self {
218 Self { data, pos: 0 }
219 }
220
221 fn remaining(&self) -> usize {
222 self.data.len().saturating_sub(self.pos)
223 }
224
225 fn read_u8(&mut self) -> Result<u8, DecoderError> {
226 if self.pos >= self.data.len() {
227 return Err(DecoderError::UnexpectedEof {
228 offset: self.pos,
229 needed: 1,
230 });
231 }
232 let val = self.data[self.pos];
233 self.pos += 1;
234 Ok(val)
235 }
236
237 fn read_i16(&mut self) -> Result<i16, DecoderError> {
238 self.check_remaining(2)?;
239 let val = i16::from_be_bytes([self.data[self.pos], self.data[self.pos + 1]]);
240 self.pos += 2;
241 Ok(val)
242 }
243
244 fn read_i32(&mut self) -> Result<i32, DecoderError> {
245 self.check_remaining(4)?;
246 let bytes: [u8; 4] = self.data[self.pos..self.pos + 4].try_into().map_err(|_| {
247 DecoderError::UnexpectedEof {
248 offset: self.pos,
249 needed: 4,
250 }
251 })?;
252 let val = i32::from_be_bytes(bytes);
253 self.pos += 4;
254 Ok(val)
255 }
256
257 fn read_u32(&mut self) -> Result<u32, DecoderError> {
258 self.check_remaining(4)?;
259 let bytes: [u8; 4] = self.data[self.pos..self.pos + 4].try_into().map_err(|_| {
260 DecoderError::UnexpectedEof {
261 offset: self.pos,
262 needed: 4,
263 }
264 })?;
265 let val = u32::from_be_bytes(bytes);
266 self.pos += 4;
267 Ok(val)
268 }
269
270 fn read_i64(&mut self) -> Result<i64, DecoderError> {
271 self.check_remaining(8)?;
272 let bytes: [u8; 8] = self.data[self.pos..self.pos + 8].try_into().map_err(|_| {
273 DecoderError::UnexpectedEof {
274 offset: self.pos,
275 needed: 8,
276 }
277 })?;
278 let val = i64::from_be_bytes(bytes);
279 self.pos += 8;
280 Ok(val)
281 }
282
283 fn read_u64(&mut self) -> Result<u64, DecoderError> {
284 self.check_remaining(8)?;
285 let bytes: [u8; 8] = self.data[self.pos..self.pos + 8].try_into().map_err(|_| {
286 DecoderError::UnexpectedEof {
287 offset: self.pos,
288 needed: 8,
289 }
290 })?;
291 let val = u64::from_be_bytes(bytes);
292 self.pos += 8;
293 Ok(val)
294 }
295
296 fn read_cstring(&mut self) -> Result<String, DecoderError> {
298 let start = self.pos;
299 let nul_pos = self.data[self.pos..]
300 .iter()
301 .position(|&b| b == 0)
302 .ok_or(DecoderError::InvalidData("unterminated string".to_string()))?;
303
304 let s = std::str::from_utf8(&self.data[self.pos..self.pos + nul_pos])
305 .map_err(|_| DecoderError::InvalidUtf8(start))?;
306
307 self.pos += nul_pos + 1; Ok(s.to_string())
309 }
310
311 fn read_bytes(&mut self, len: usize) -> Result<Bytes, DecoderError> {
312 self.check_remaining(len)?;
313 let slice = self.data.slice(self.pos..self.pos + len);
314 self.pos += len;
315 Ok(slice)
316 }
317
318 fn check_remaining(&self, needed: usize) -> Result<(), DecoderError> {
319 if self.remaining() < needed {
320 return Err(DecoderError::UnexpectedEof {
321 offset: self.pos,
322 needed,
323 });
324 }
325 Ok(())
326 }
327}
328
329pub(super) fn pg_timestamp_to_unix_ms(pg_us: i64) -> Result<i64, DecoderError> {
336 pg_us
337 .checked_add(PG_EPOCH_OFFSET_US)
338 .map(|unix_us| unix_us.div_euclid(1000))
339 .ok_or_else(|| DecoderError::InvalidData("PostgreSQL timestamp overflow".into()))
340}
341
342pub(super) fn decode_message(data: Bytes) -> Result<WalMessage, DecoderError> {
349 if data.is_empty() {
350 return Err(DecoderError::InvalidData("empty message".to_string()));
351 }
352
353 let mut cur = Cursor::new(data);
354 let msg_type = cur.read_u8()?;
355
356 let message = match msg_type {
357 b'B' => decode_begin(&mut cur),
358 b'C' => decode_commit(&mut cur),
359 b'R' => decode_relation(&mut cur),
360 b'I' => decode_insert(&mut cur),
361 b'U' => decode_update(&mut cur),
362 b'D' => decode_delete(&mut cur),
363 b'T' => decode_truncate(&mut cur),
364 b'O' => decode_origin(&mut cur),
365 b'Y' => decode_type(&mut cur),
366 _ => Err(DecoderError::UnknownMessageType(msg_type)),
367 }?;
368 if cur.remaining() != 0 {
369 return Err(DecoderError::InvalidData(format!(
370 "trailing bytes after pgoutput message: {}",
371 cur.remaining()
372 )));
373 }
374 Ok(message)
375}
376
377fn decode_begin(cur: &mut Cursor) -> Result<WalMessage, DecoderError> {
378 let final_lsn = Lsn::new(cur.read_u64()?);
379 let commit_ts_us = cur.read_i64()?;
380 let xid = cur.read_u32()?;
381 Ok(WalMessage::Begin(BeginMessage {
382 final_lsn,
383 commit_ts_ms: pg_timestamp_to_unix_ms(commit_ts_us)?,
384 xid,
385 }))
386}
387
388fn decode_commit(cur: &mut Cursor) -> Result<WalMessage, DecoderError> {
389 let flags = cur.read_u8()?;
390 if flags != 0 {
391 return Err(DecoderError::InvalidData(format!(
392 "unsupported COMMIT flags: 0x{flags:02X}"
393 )));
394 }
395 let commit_lsn = Lsn::new(cur.read_u64()?);
396 let end_lsn = Lsn::new(cur.read_u64()?);
397 let commit_ts_us = cur.read_i64()?;
398 Ok(WalMessage::Commit(CommitMessage {
399 flags,
400 commit_lsn,
401 end_lsn,
402 commit_ts_ms: pg_timestamp_to_unix_ms(commit_ts_us)?,
403 }))
404}
405
406fn decode_relation(cur: &mut Cursor) -> Result<WalMessage, DecoderError> {
407 let relation_id = cur.read_u32()?;
408 let namespace = cur.read_cstring()?;
409 let name = cur.read_cstring()?;
410 let replica_identity = cur.read_u8()?;
411 let n_cols_raw = cur.read_i16()?;
412 let n_cols = usize::try_from(n_cols_raw)
413 .map_err(|_| DecoderError::InvalidData(format!("negative column count: {n_cols_raw}")))?;
414 validate_column_count(n_cols)?;
415
416 let mut columns = Vec::with_capacity(n_cols);
417 for _ in 0..n_cols {
418 let flags = cur.read_u8()?;
419 let col_name = cur.read_cstring()?;
420 let type_oid = cur.read_u32()?;
421 let type_modifier = cur.read_i32()?;
422 columns.push(PgColumn::new(
423 col_name,
424 type_oid,
425 type_modifier,
426 flags & 1 != 0,
427 ));
428 }
429
430 Ok(WalMessage::Relation(RelationMessage {
431 relation_id,
432 namespace,
433 name,
434 replica_identity,
435 columns,
436 }))
437}
438
439fn decode_insert(cur: &mut Cursor) -> Result<WalMessage, DecoderError> {
440 let relation_id = cur.read_u32()?;
441 let tag = cur.read_u8()?;
442 if tag != b'N' {
443 return Err(DecoderError::InvalidData(format!(
444 "expected 'N' tag in INSERT, got 0x{tag:02X}"
445 )));
446 }
447 let new_tuple = decode_tuple_data(cur)?;
448 Ok(WalMessage::Insert(InsertMessage {
449 relation_id,
450 new_tuple,
451 }))
452}
453
454fn decode_update(cur: &mut Cursor) -> Result<WalMessage, DecoderError> {
455 let relation_id = cur.read_u32()?;
456 let tag = cur.read_u8()?;
457
458 let (old_tuple, new_tuple) = match tag {
459 b'N' => (None, decode_tuple_data(cur)?),
461 b'K' | b'O' => {
463 let old_data = decode_tuple_data(cur)?;
464 let old = if tag == b'K' {
465 OldTuple::Key(old_data)
466 } else {
467 OldTuple::Full(old_data)
468 };
469 let new_tag = cur.read_u8()?;
470 if new_tag != b'N' {
471 return Err(DecoderError::InvalidData(format!(
472 "expected 'N' tag after old tuple in UPDATE, got 0x{new_tag:02X}"
473 )));
474 }
475 let new = decode_tuple_data(cur)?;
476 (Some(old), new)
477 }
478 _ => {
479 return Err(DecoderError::InvalidData(format!(
480 "unexpected tag in UPDATE: 0x{tag:02X}"
481 )));
482 }
483 };
484
485 Ok(WalMessage::Update(UpdateMessage {
486 relation_id,
487 old_tuple,
488 new_tuple,
489 }))
490}
491
492fn decode_delete(cur: &mut Cursor) -> Result<WalMessage, DecoderError> {
493 let relation_id = cur.read_u32()?;
494 let tag = cur.read_u8()?;
495 if tag != b'K' && tag != b'O' {
496 return Err(DecoderError::InvalidData(format!(
497 "expected 'K' or 'O' tag in DELETE, got 0x{tag:02X}"
498 )));
499 }
500 let old_data = decode_tuple_data(cur)?;
501 let old_tuple = if tag == b'K' {
502 OldTuple::Key(old_data)
503 } else {
504 OldTuple::Full(old_data)
505 };
506 Ok(WalMessage::Delete(DeleteMessage {
507 relation_id,
508 old_tuple,
509 }))
510}
511
512fn decode_truncate(cur: &mut Cursor) -> Result<WalMessage, DecoderError> {
513 let n_relations_raw = cur.read_u32()?;
514 let n_relations = usize::try_from(n_relations_raw).map_err(|_| {
515 DecoderError::InvalidData(format!(
516 "TRUNCATE relation count {n_relations_raw} does not fit this platform"
517 ))
518 })?;
519 let options = cur.read_u8()?;
520 let relation_bytes = n_relations
521 .checked_mul(std::mem::size_of::<u32>())
522 .ok_or_else(|| DecoderError::InvalidData("TRUNCATE relation count overflow".into()))?;
523 if relation_bytes != cur.remaining() {
524 return Err(DecoderError::InvalidData(format!(
525 "TRUNCATE relation count {n_relations} requires {relation_bytes} bytes, but {} remain",
526 cur.remaining()
527 )));
528 }
529 let mut relation_ids = Vec::with_capacity(n_relations);
530 for _ in 0..n_relations {
531 relation_ids.push(cur.read_u32()?);
532 }
533 Ok(WalMessage::Truncate(TruncateMessage {
534 relation_ids,
535 options,
536 }))
537}
538
539fn decode_origin(cur: &mut Cursor) -> Result<WalMessage, DecoderError> {
540 let origin_lsn = Lsn::new(cur.read_u64()?);
541 let name = cur.read_cstring()?;
542 Ok(WalMessage::Origin(OriginMessage { origin_lsn, name }))
543}
544
545fn decode_type(cur: &mut Cursor) -> Result<WalMessage, DecoderError> {
546 let type_id = cur.read_u32()?;
547 let namespace = cur.read_cstring()?;
548 let name = cur.read_cstring()?;
549 Ok(WalMessage::Type(TypeMessage {
550 type_id,
551 namespace,
552 name,
553 }))
554}
555
556fn decode_tuple_data(cur: &mut Cursor) -> Result<TupleData, DecoderError> {
557 let n_cols_raw = cur.read_i16()?;
558 let n_cols = usize::try_from(n_cols_raw)
559 .map_err(|_| DecoderError::InvalidData(format!("negative column count: {n_cols_raw}")))?;
560 validate_column_count(n_cols)?;
561 let mut columns = Vec::with_capacity(n_cols);
562
563 for _ in 0..n_cols {
564 let col_type = cur.read_u8()?;
565 match col_type {
566 b'n' => columns.push(ColumnValue::Null),
567 b'u' => columns.push(ColumnValue::Unchanged),
568 b't' => {
569 let len_raw = cur.read_i32()?;
570 let len = usize::try_from(len_raw).map_err(|_| {
571 DecoderError::InvalidData(format!("negative text length: {len_raw}"))
572 })?;
573 let data = cur.read_bytes(len)?;
574 std::str::from_utf8(&data).map_err(|_| DecoderError::InvalidUtf8(cur.pos - len))?;
575 columns.push(ColumnValue::Text(data));
576 }
577 _ => {
578 return Err(DecoderError::InvalidData(format!(
579 "unknown column type: 0x{col_type:02X}"
580 )));
581 }
582 }
583 }
584
585 Ok(TupleData { columns })
586}
587
588fn validate_column_count(count: usize) -> Result<(), DecoderError> {
589 if count > MAX_POSTGRES_COLUMNS {
590 return Err(DecoderError::InvalidData(format!(
591 "column count {count} exceeds PostgreSQL maximum {MAX_POSTGRES_COLUMNS}"
592 )));
593 }
594 Ok(())
595}
596
597#[cfg(test)]
598mod tests {
599 use super::*;
600
601 fn decode_message(data: &[u8]) -> Result<WalMessage, DecoderError> {
602 super::decode_message(Bytes::copy_from_slice(data))
603 }
604
605 struct MessageBuilder {
609 buf: Vec<u8>,
610 }
611
612 impl MessageBuilder {
613 fn new(msg_type: u8) -> Self {
614 Self {
615 buf: vec![msg_type],
616 }
617 }
618
619 fn u8(mut self, v: u8) -> Self {
620 self.buf.push(v);
621 self
622 }
623
624 fn i16(mut self, v: i16) -> Self {
625 self.buf.extend_from_slice(&v.to_be_bytes());
626 self
627 }
628
629 fn i32(mut self, v: i32) -> Self {
630 self.buf.extend_from_slice(&v.to_be_bytes());
631 self
632 }
633
634 fn u32(mut self, v: u32) -> Self {
635 self.buf.extend_from_slice(&v.to_be_bytes());
636 self
637 }
638
639 fn i64(mut self, v: i64) -> Self {
640 self.buf.extend_from_slice(&v.to_be_bytes());
641 self
642 }
643
644 fn u64(mut self, v: u64) -> Self {
645 self.buf.extend_from_slice(&v.to_be_bytes());
646 self
647 }
648
649 fn cstring(mut self, s: &str) -> Self {
650 self.buf.extend_from_slice(s.as_bytes());
651 self.buf.push(0);
652 self
653 }
654
655 fn text_col(mut self, s: &str) -> Self {
656 self.buf.push(b't');
657 self.buf.extend_from_slice(&(s.len() as i32).to_be_bytes());
658 self.buf.extend_from_slice(s.as_bytes());
659 self
660 }
661
662 fn null_col(mut self) -> Self {
663 self.buf.push(b'n');
664 self
665 }
666
667 fn unchanged_col(mut self) -> Self {
668 self.buf.push(b'u');
669 self
670 }
671
672 fn build(self) -> Vec<u8> {
673 self.buf
674 }
675 }
676
677 #[test]
680 fn test_decode_begin() {
681 let pg_ts_us: i64 = 757_382_400_000_000;
684 let data = MessageBuilder::new(b'B')
685 .u64(0x1234_ABCD) .i64(pg_ts_us) .u32(42) .build();
689
690 let msg = decode_message(&data).unwrap();
691 match msg {
692 WalMessage::Begin(b) => {
693 assert_eq!(b.final_lsn.as_u64(), 0x1234_ABCD);
694 assert_eq!(b.xid, 42);
695 assert_eq!(b.commit_ts_ms, (pg_ts_us + PG_EPOCH_OFFSET_US) / 1000);
696 }
697 _ => panic!("expected Begin"),
698 }
699 }
700
701 #[test]
704 fn test_decode_commit() {
705 let pg_ts_us: i64 = 757_382_400_000_000;
706 let data = MessageBuilder::new(b'C')
707 .u8(0) .u64(0x100) .u64(0x200) .i64(pg_ts_us) .build();
712
713 let msg = decode_message(&data).unwrap();
714 match msg {
715 WalMessage::Commit(c) => {
716 assert_eq!(c.flags, 0);
717 assert_eq!(c.commit_lsn.as_u64(), 0x100);
718 assert_eq!(c.end_lsn.as_u64(), 0x200);
719 }
720 _ => panic!("expected Commit"),
721 }
722 }
723
724 #[test]
727 fn test_decode_relation() {
728 let data = MessageBuilder::new(b'R')
729 .u32(16384) .cstring("public") .cstring("users") .u8(b'd') .i16(2) .u8(1) .cstring("id")
737 .u32(20) .i32(-1) .u8(0) .cstring("name")
742 .u32(25) .i32(-1)
744 .build();
745
746 let msg = decode_message(&data).unwrap();
747 match msg {
748 WalMessage::Relation(r) => {
749 assert_eq!(r.relation_id, 16384);
750 assert_eq!(r.namespace, "public");
751 assert_eq!(r.name, "users");
752 assert_eq!(r.replica_identity, b'd');
753 assert_eq!(r.columns.len(), 2);
754 assert_eq!(r.columns[0].name, "id");
755 assert!(r.columns[0].is_key);
756 assert_eq!(r.columns[0].type_oid, 20);
757 assert_eq!(r.columns[1].name, "name");
758 assert!(!r.columns[1].is_key);
759 }
760 _ => panic!("expected Relation"),
761 }
762 }
763
764 #[test]
767 fn test_decode_insert() {
768 let data = MessageBuilder::new(b'I')
769 .u32(16384) .u8(b'N') .i16(3) .text_col("42") .text_col("Alice") .null_col() .build();
776
777 let msg = decode_message(&data).unwrap();
778 match msg {
779 WalMessage::Insert(ins) => {
780 assert_eq!(ins.relation_id, 16384);
781 assert_eq!(ins.new_tuple.columns.len(), 3);
782 assert_eq!(ins.new_tuple.columns[0].as_text(), Some("42"));
783 assert_eq!(ins.new_tuple.columns[1].as_text(), Some("Alice"));
784 assert!(ins.new_tuple.columns[2].is_null());
785 }
786 _ => panic!("expected Insert"),
787 }
788 }
789
790 #[test]
793 fn test_decode_update_no_old() {
794 let data = MessageBuilder::new(b'U')
795 .u32(16384)
796 .u8(b'N') .i16(2)
798 .text_col("42")
799 .text_col("Bob")
800 .build();
801
802 let msg = decode_message(&data).unwrap();
803 match msg {
804 WalMessage::Update(upd) => {
805 assert!(upd.old_tuple.is_none());
806 assert_eq!(upd.new_tuple.columns[1].as_text(), Some("Bob"));
807 }
808 _ => panic!("expected Update"),
809 }
810 }
811
812 #[test]
815 fn test_decode_update_with_old() {
816 let data = MessageBuilder::new(b'U')
817 .u32(16384)
818 .u8(b'O') .i16(2) .text_col("42")
821 .text_col("Alice")
822 .u8(b'N') .i16(2) .text_col("42")
825 .text_col("Bob")
826 .build();
827
828 let msg = decode_message(&data).unwrap();
829 match msg {
830 WalMessage::Update(upd) => {
831 let Some(OldTuple::Full(old)) = upd.old_tuple else {
832 panic!("expected full old tuple");
833 };
834 assert_eq!(old.columns[1].as_text(), Some("Alice"));
835 assert_eq!(upd.new_tuple.columns[1].as_text(), Some("Bob"));
836 }
837 _ => panic!("expected Update"),
838 }
839 }
840
841 #[test]
844 fn test_decode_delete_key() {
845 let data = MessageBuilder::new(b'D')
846 .u32(16384)
847 .u8(b'K') .i16(1)
849 .text_col("42")
850 .build();
851
852 let msg = decode_message(&data).unwrap();
853 match msg {
854 WalMessage::Delete(del) => {
855 assert_eq!(del.relation_id, 16384);
856 let OldTuple::Key(old) = del.old_tuple else {
857 panic!("expected key old tuple");
858 };
859 assert_eq!(old.columns[0].as_text(), Some("42"));
860 }
861 _ => panic!("expected Delete"),
862 }
863 }
864
865 #[test]
866 fn test_decode_delete_full() {
867 let data = MessageBuilder::new(b'D')
868 .u32(16384)
869 .u8(b'O') .i16(2)
871 .text_col("42")
872 .text_col("Alice")
873 .build();
874
875 let msg = decode_message(&data).unwrap();
876 match msg {
877 WalMessage::Delete(del) => {
878 let OldTuple::Full(old) = del.old_tuple else {
879 panic!("expected full old tuple");
880 };
881 assert_eq!(old.columns.len(), 2);
882 }
883 _ => panic!("expected Delete"),
884 }
885 }
886
887 #[test]
890 fn test_decode_truncate() {
891 let data = MessageBuilder::new(b'T')
892 .u32(2) .u8(1) .u32(16384) .u32(16385) .build();
897
898 let msg = decode_message(&data).unwrap();
899 match msg {
900 WalMessage::Truncate(t) => {
901 assert_eq!(t.relation_ids, vec![16384, 16385]);
902 assert_eq!(t.options, 1);
903 }
904 _ => panic!("expected Truncate"),
905 }
906 }
907
908 #[test]
911 fn test_decode_origin() {
912 let data = MessageBuilder::new(b'O')
913 .u64(0xABCD)
914 .cstring("upstream")
915 .build();
916
917 let msg = decode_message(&data).unwrap();
918 match msg {
919 WalMessage::Origin(o) => {
920 assert_eq!(o.origin_lsn.as_u64(), 0xABCD);
921 assert_eq!(o.name, "upstream");
922 }
923 _ => panic!("expected Origin"),
924 }
925 }
926
927 #[test]
930 fn test_decode_type() {
931 let data = MessageBuilder::new(b'Y')
932 .u32(12345)
933 .cstring("public")
934 .cstring("my_enum")
935 .build();
936
937 let msg = decode_message(&data).unwrap();
938 match msg {
939 WalMessage::Type(t) => {
940 assert_eq!(t.type_id, 12345);
941 assert_eq!(t.namespace, "public");
942 assert_eq!(t.name, "my_enum");
943 }
944 _ => panic!("expected Type"),
945 }
946 }
947
948 #[test]
951 fn test_decode_insert_with_unchanged() {
952 let data = MessageBuilder::new(b'I')
953 .u32(16384)
954 .u8(b'N')
955 .i16(2)
956 .text_col("42")
957 .unchanged_col()
958 .build();
959
960 let msg = decode_message(&data).unwrap();
961 match msg {
962 WalMessage::Insert(ins) => {
963 assert_eq!(ins.new_tuple.columns[0].as_text(), Some("42"));
964 assert!(matches!(ins.new_tuple.columns[1], ColumnValue::Unchanged));
965 }
966 _ => panic!("expected Insert"),
967 }
968 }
969
970 #[test]
973 fn test_decode_empty_data() {
974 assert!(decode_message(&[]).is_err());
975 }
976
977 #[test]
978 fn test_decode_unknown_type() {
979 let err = decode_message(&[0xFF]).unwrap_err();
980 assert!(matches!(err, DecoderError::UnknownMessageType(0xFF)));
981 }
982
983 #[test]
984 fn test_decode_truncated_begin() {
985 let data = MessageBuilder::new(b'B').u32(0).build();
987 assert!(decode_message(&data).is_err());
988 }
989
990 #[test]
991 fn test_decode_invalid_insert_tag() {
992 let data = MessageBuilder::new(b'I')
993 .u32(16384)
994 .u8(b'X') .build();
996 assert!(decode_message(&data).is_err());
997 }
998
999 #[test]
1000 fn nonzero_commit_flags_are_rejected() {
1001 let data = MessageBuilder::new(b'C')
1002 .u8(1)
1003 .u64(0x100)
1004 .u64(0x200)
1005 .i64(0)
1006 .build();
1007 let error = decode_message(&data).unwrap_err();
1008 assert!(error.to_string().contains("COMMIT flags"), "{error}");
1009 }
1010
1011 #[test]
1012 fn trailing_bytes_are_rejected() {
1013 let mut data = MessageBuilder::new(b'B').u64(1).i64(0).u32(1).build();
1014 data.push(0xff);
1015 let error = decode_message(&data).unwrap_err();
1016 assert!(error.to_string().contains("trailing bytes"), "{error}");
1017 }
1018
1019 #[test]
1022 fn test_pg_timestamp_to_unix_ms() {
1023 assert_eq!(pg_timestamp_to_unix_ms(0).unwrap(), 946_684_800_000);
1026
1027 let pg_us: i64 = 757_382_400_000_000;
1030 let expected_unix_ms = (pg_us + PG_EPOCH_OFFSET_US).div_euclid(1000);
1031 assert_eq!(pg_timestamp_to_unix_ms(pg_us).unwrap(), expected_unix_ms);
1032
1033 assert_eq!(
1035 pg_timestamp_to_unix_ms(-PG_EPOCH_OFFSET_US - 1).unwrap(),
1036 -1
1037 );
1038 assert_eq!(
1039 pg_timestamp_to_unix_ms(-PG_EPOCH_OFFSET_US - 1_001).unwrap(),
1040 -2
1041 );
1042 assert!(pg_timestamp_to_unix_ms(i64::MAX).is_err());
1043 }
1044
1045 #[test]
1048 fn test_column_value_as_text() {
1049 let text = ColumnValue::Text(Bytes::from_static(b"hello"));
1050 assert_eq!(text.as_text(), Some("hello"));
1051 assert!(!text.is_null());
1052
1053 let null = ColumnValue::Null;
1054 assert_eq!(null.as_text(), None);
1055 assert!(null.is_null());
1056
1057 let unchanged = ColumnValue::Unchanged;
1058 assert_eq!(unchanged.as_text(), None);
1059 assert!(!unchanged.is_null());
1060 }
1061
1062 #[test]
1065 fn test_decode_update_with_key_identity() {
1066 let data = MessageBuilder::new(b'U')
1067 .u32(16384)
1068 .u8(b'K') .i16(2) .text_col("42")
1071 .null_col() .u8(b'N') .i16(2) .text_col("42")
1075 .text_col("Updated")
1076 .build();
1077
1078 let msg = decode_message(&data).unwrap();
1079 match msg {
1080 WalMessage::Update(upd) => {
1081 let Some(OldTuple::Key(old)) = upd.old_tuple else {
1082 panic!("expected key old tuple");
1083 };
1084 assert_eq!(old.columns.len(), 2);
1085 assert!(old.columns[1].is_null());
1086 assert_eq!(upd.new_tuple.columns.len(), 2);
1087 }
1088 _ => panic!("expected Update"),
1089 }
1090 }
1091
1092 #[test]
1093 fn tuple_text_is_a_zero_copy_slice_of_the_wal_frame() {
1094 let frame = Bytes::from(
1095 MessageBuilder::new(b'I')
1096 .u32(16_384)
1097 .u8(b'N')
1098 .i16(1)
1099 .text_col("allocation-free")
1100 .build(),
1101 );
1102 let start = frame.as_ptr() as usize;
1103 let end = start + frame.len();
1104
1105 let message = super::decode_message(frame).unwrap();
1106 let WalMessage::Insert(insert) = message else {
1107 panic!("expected insert");
1108 };
1109 let ColumnValue::Text(value) = &insert.new_tuple.columns[0] else {
1110 panic!("expected text");
1111 };
1112 let value_ptr = value.as_ptr() as usize;
1113 assert!(value_ptr >= start && value_ptr + value.len() <= end);
1114 assert_eq!(
1115 insert.new_tuple.columns[0].as_text(),
1116 Some("allocation-free")
1117 );
1118 }
1119
1120 #[test]
1121 fn tuple_column_count_above_postgres_limit_is_rejected_before_values() {
1122 let data = MessageBuilder::new(b'I')
1123 .u32(16_384)
1124 .u8(b'N')
1125 .i16((MAX_POSTGRES_COLUMNS + 1) as i16)
1126 .build();
1127 let error = decode_message(&data).unwrap_err();
1128 assert!(error.to_string().contains("PostgreSQL maximum"), "{error}");
1129 }
1130
1131 #[test]
1132 fn maximum_postgres_tuple_column_count_is_accepted() {
1133 let mut data = MessageBuilder::new(b'I')
1134 .u32(16_384)
1135 .u8(b'N')
1136 .i16(MAX_POSTGRES_COLUMNS as i16)
1137 .build();
1138 data.extend(std::iter::repeat_n(b'n', MAX_POSTGRES_COLUMNS));
1139
1140 let message = decode_message(&data).unwrap();
1141 let WalMessage::Insert(insert) = message else {
1142 panic!("expected insert");
1143 };
1144 assert_eq!(insert.new_tuple.columns.len(), MAX_POSTGRES_COLUMNS);
1145 }
1146
1147 #[test]
1148 fn invalid_tuple_utf8_is_rejected() {
1149 let mut data = MessageBuilder::new(b'I')
1150 .u32(16_384)
1151 .u8(b'N')
1152 .i16(1)
1153 .build();
1154 data.push(b't');
1155 data.extend_from_slice(&1_i32.to_be_bytes());
1156 data.push(0xff);
1157
1158 assert!(matches!(
1159 decode_message(&data),
1160 Err(DecoderError::InvalidUtf8(_))
1161 ));
1162 }
1163
1164 #[test]
1165 fn truncate_count_is_validated_before_allocation() {
1166 let data = MessageBuilder::new(b'T').u32(u32::MAX).u8(0).build();
1167
1168 let error = decode_message(&data).unwrap_err();
1169 assert!(error.to_string().contains("requires"), "{error}");
1170 }
1171}