Skip to main content

pgsync_postgres/replication/
pgoutput.rs

1//! Decoder for the CopyBoth payloads of logical replication: XLogData / keepalive framing
2//! and the pgoutput protocol version 1 message set.
3//!
4//! Byte layouts follow `docs/protocol-reference.md`. Tuple values are zero-copy slices
5//! of the incoming buffer.
6
7use bytes::{Buf, Bytes};
8use pgsync_core::event::{Column, Relation, ReplicaIdentity, Tuple, Value};
9use pgsync_core::{Error, Lsn, Result};
10
11/// Outer replication message.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum ReplicationMessage {
14    XLogData {
15        wal_start: Lsn,
16        wal_end: Lsn,
17        server_clock: i64,
18        body: LogicalMessage,
19    },
20    PrimaryKeepalive {
21        wal_end: Lsn,
22        server_clock: i64,
23        reply_requested: bool,
24    },
25}
26
27/// pgoutput message.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum LogicalMessage {
30    Begin {
31        final_lsn: Lsn,
32        commit_ts: i64,
33        xid: u32,
34    },
35    Commit {
36        flags: u8,
37        commit_lsn: Lsn,
38        end_lsn: Lsn,
39        commit_ts: i64,
40    },
41    Origin {
42        commit_lsn: Lsn,
43        name: String,
44    },
45    Relation(Relation),
46    Type {
47        oid: u32,
48        schema: String,
49        name: String,
50    },
51    Insert {
52        relation: u32,
53        new: Tuple,
54    },
55    Update {
56        relation: u32,
57        key: Option<Tuple>,
58        old: Option<Tuple>,
59        new: Tuple,
60    },
61    Delete {
62        relation: u32,
63        key: Option<Tuple>,
64        old: Option<Tuple>,
65    },
66    Truncate {
67        relations: Vec<u32>,
68        cascade: bool,
69        restart_identity: bool,
70    },
71    Message {
72        transactional: bool,
73        lsn: Lsn,
74        prefix: String,
75        content: Bytes,
76    },
77    /// A tag this decoder does not implement (streaming, two-phase, ...). The engine stops.
78    Unsupported {
79        tag: u8,
80    },
81}
82
83pub fn decode(mut buf: Bytes) -> Result<ReplicationMessage> {
84    let tag = take_u8(&mut buf, "replication message tag")?;
85    match tag {
86        b'w' => {
87            let wal_start = Lsn(take_u64(&mut buf, "wal_start")?);
88            let wal_end = Lsn(take_u64(&mut buf, "wal_end")?);
89            let server_clock = take_i64(&mut buf, "server_clock")?;
90            let body = decode_logical(buf)?;
91            Ok(ReplicationMessage::XLogData {
92                wal_start,
93                wal_end,
94                server_clock,
95                body,
96            })
97        }
98        b'k' => {
99            let wal_end = Lsn(take_u64(&mut buf, "wal_end")?);
100            let server_clock = take_i64(&mut buf, "server_clock")?;
101            let reply_requested = take_u8(&mut buf, "reply")? != 0;
102            Ok(ReplicationMessage::PrimaryKeepalive {
103                wal_end,
104                server_clock,
105                reply_requested,
106            })
107        }
108        other => Err(Error::internal(format!(
109            "unknown replication message tag {:?}",
110            other as char
111        ))),
112    }
113}
114
115pub fn decode_logical(mut buf: Bytes) -> Result<LogicalMessage> {
116    let tag = take_u8(&mut buf, "pgoutput tag")?;
117    let msg = match tag {
118        b'B' => LogicalMessage::Begin {
119            final_lsn: Lsn(take_u64(&mut buf, "final_lsn")?),
120            commit_ts: take_i64(&mut buf, "commit_ts")?,
121            xid: take_u32(&mut buf, "xid")?,
122        },
123        b'C' => LogicalMessage::Commit {
124            flags: take_u8(&mut buf, "flags")?,
125            commit_lsn: Lsn(take_u64(&mut buf, "commit_lsn")?),
126            end_lsn: Lsn(take_u64(&mut buf, "end_lsn")?),
127            commit_ts: take_i64(&mut buf, "commit_ts")?,
128        },
129        b'O' => LogicalMessage::Origin {
130            commit_lsn: Lsn(take_u64(&mut buf, "origin lsn")?),
131            name: take_cstr(&mut buf, "origin name")?,
132        },
133        b'R' => {
134            let id = take_u32(&mut buf, "relation id")?;
135            let schema = take_cstr(&mut buf, "namespace")?;
136            let name = take_cstr(&mut buf, "relation name")?;
137            let ident = take_u8(&mut buf, "replica identity")?;
138            let replica_identity = ReplicaIdentity::from_char(ident).ok_or_else(|| {
139                Error::internal(format!("unknown replica identity {:?}", ident as char))
140            })?;
141            let ncols = take_u16(&mut buf, "column count")? as usize;
142            let mut columns = Vec::with_capacity(ncols);
143            for _ in 0..ncols {
144                let flags = take_u8(&mut buf, "column flags")?;
145                let name = take_cstr(&mut buf, "column name")?;
146                let type_oid = take_u32(&mut buf, "column type")?;
147                let type_modifier = take_i32(&mut buf, "column typmod")?;
148                columns.push(Column {
149                    name,
150                    type_oid,
151                    type_modifier,
152                    is_key: flags & 1 == 1,
153                });
154            }
155            LogicalMessage::Relation(Relation {
156                id,
157                schema: if schema.is_empty() {
158                    "pg_catalog".to_owned()
159                } else {
160                    schema
161                },
162                name,
163                replica_identity,
164                columns,
165            })
166        }
167        b'Y' => LogicalMessage::Type {
168            oid: take_u32(&mut buf, "type oid")?,
169            schema: take_cstr(&mut buf, "type namespace")?,
170            name: take_cstr(&mut buf, "type name")?,
171        },
172        b'I' => {
173            let relation = take_u32(&mut buf, "relation id")?;
174            let kind = take_u8(&mut buf, "tuple kind")?;
175            if kind != b'N' {
176                return Err(Error::internal(format!(
177                    "insert: expected 'N' tuple, got {:?}",
178                    kind as char
179                )));
180            }
181            LogicalMessage::Insert {
182                relation,
183                new: take_tuple(&mut buf)?,
184            }
185        }
186        b'U' => {
187            let relation = take_u32(&mut buf, "relation id")?;
188            let mut key = None;
189            let mut old = None;
190            let mut kind = take_u8(&mut buf, "tuple kind")?;
191            if kind == b'K' {
192                key = Some(take_tuple(&mut buf)?);
193                kind = take_u8(&mut buf, "tuple kind")?;
194            } else if kind == b'O' {
195                old = Some(take_tuple(&mut buf)?);
196                kind = take_u8(&mut buf, "tuple kind")?;
197            }
198            if kind != b'N' {
199                return Err(Error::internal(format!(
200                    "update: expected 'N' tuple, got {:?}",
201                    kind as char
202                )));
203            }
204            LogicalMessage::Update {
205                relation,
206                key,
207                old,
208                new: take_tuple(&mut buf)?,
209            }
210        }
211        b'D' => {
212            let relation = take_u32(&mut buf, "relation id")?;
213            let kind = take_u8(&mut buf, "tuple kind")?;
214            let tuple = take_tuple(&mut buf)?;
215            match kind {
216                b'K' => LogicalMessage::Delete {
217                    relation,
218                    key: Some(tuple),
219                    old: None,
220                },
221                b'O' => LogicalMessage::Delete {
222                    relation,
223                    key: None,
224                    old: Some(tuple),
225                },
226                other => {
227                    return Err(Error::internal(format!(
228                        "delete: unexpected tuple kind {:?}",
229                        other as char
230                    )));
231                }
232            }
233        }
234        b'T' => {
235            let n = take_u32(&mut buf, "relation count")? as usize;
236            let options = take_u8(&mut buf, "truncate options")?;
237            let mut relations = Vec::with_capacity(n);
238            for _ in 0..n {
239                relations.push(take_u32(&mut buf, "relation id")?);
240            }
241            LogicalMessage::Truncate {
242                relations,
243                cascade: options & 1 != 0,
244                restart_identity: options & 2 != 0,
245            }
246        }
247        b'M' => {
248            let flags = take_u8(&mut buf, "message flags")?;
249            let lsn = Lsn(take_u64(&mut buf, "message lsn")?);
250            let prefix = take_cstr(&mut buf, "message prefix")?;
251            let len = take_u32(&mut buf, "message length")? as usize;
252            if buf.len() < len {
253                return Err(truncated("message content"));
254            }
255            let content = buf.split_to(len);
256            LogicalMessage::Message {
257                transactional: flags & 1 != 0,
258                lsn,
259                prefix,
260                content,
261            }
262        }
263        other => LogicalMessage::Unsupported { tag: other },
264    };
265    Ok(msg)
266}
267
268fn take_tuple(buf: &mut Bytes) -> Result<Tuple> {
269    let n = take_u16(buf, "tuple column count")? as usize;
270    let mut values = Vec::with_capacity(n);
271    for _ in 0..n {
272        let kind = take_u8(buf, "tuple value kind")?;
273        let v = match kind {
274            b'n' => Value::Null,
275            b'u' => Value::Unchanged,
276            b't' | b'b' => {
277                let len = take_u32(buf, "value length")? as usize;
278                if buf.len() < len {
279                    return Err(truncated("tuple value"));
280                }
281                let data = buf.split_to(len);
282                if kind == b't' {
283                    Value::Text(data)
284                } else {
285                    Value::Binary(data)
286                }
287            }
288            other => {
289                return Err(Error::internal(format!(
290                    "unknown tuple value kind {:?}",
291                    other as char
292                )));
293            }
294        };
295        values.push(v);
296    }
297    Ok(Tuple { values })
298}
299
300fn truncated(what: &str) -> Error {
301    Error::internal(format!("truncated pgoutput message while reading {what}"))
302}
303
304fn take_u8(buf: &mut Bytes, what: &str) -> Result<u8> {
305    if buf.remaining() < 1 {
306        return Err(truncated(what));
307    }
308    Ok(buf.get_u8())
309}
310fn take_u16(buf: &mut Bytes, what: &str) -> Result<u16> {
311    if buf.remaining() < 2 {
312        return Err(truncated(what));
313    }
314    Ok(buf.get_u16())
315}
316fn take_u32(buf: &mut Bytes, what: &str) -> Result<u32> {
317    if buf.remaining() < 4 {
318        return Err(truncated(what));
319    }
320    Ok(buf.get_u32())
321}
322fn take_i32(buf: &mut Bytes, what: &str) -> Result<i32> {
323    Ok(take_u32(buf, what)? as i32)
324}
325fn take_u64(buf: &mut Bytes, what: &str) -> Result<u64> {
326    if buf.remaining() < 8 {
327        return Err(truncated(what));
328    }
329    Ok(buf.get_u64())
330}
331fn take_i64(buf: &mut Bytes, what: &str) -> Result<i64> {
332    Ok(take_u64(buf, what)? as i64)
333}
334fn take_cstr(buf: &mut Bytes, what: &str) -> Result<String> {
335    let end = buf
336        .iter()
337        .position(|b| *b == 0)
338        .ok_or_else(|| truncated(what))?;
339    let s = buf.split_to(end);
340    buf.advance(1);
341    String::from_utf8(s.to_vec()).map_err(|_| Error::internal(format!("{what}: invalid UTF-8")))
342}
343
344/// Encode a standby status update (`'r'`).
345pub fn encode_standby_status(
346    written: Lsn,
347    flushed: Lsn,
348    applied: Lsn,
349    clock: i64,
350    reply: bool,
351) -> [u8; 34] {
352    let mut out = [0u8; 34];
353    out[0] = b'r';
354    out[1..9].copy_from_slice(&written.0.to_be_bytes());
355    out[9..17].copy_from_slice(&flushed.0.to_be_bytes());
356    out[17..25].copy_from_slice(&applied.0.to_be_bytes());
357    out[25..33].copy_from_slice(&clock.to_be_bytes());
358    out[33] = reply as u8;
359    out
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365
366    fn b(v: &[&[u8]]) -> Bytes {
367        Bytes::from(v.concat())
368    }
369    fn u64be(v: u64) -> Vec<u8> {
370        v.to_be_bytes().to_vec()
371    }
372    fn u32be(v: u32) -> Vec<u8> {
373        v.to_be_bytes().to_vec()
374    }
375    fn u16be(v: u16) -> Vec<u8> {
376        v.to_be_bytes().to_vec()
377    }
378    fn cstr(s: &str) -> Vec<u8> {
379        let mut v = s.as_bytes().to_vec();
380        v.push(0);
381        v
382    }
383    fn text(s: &str) -> Vec<u8> {
384        let mut v = vec![b't'];
385        v.extend(u32be(s.len() as u32));
386        v.extend(s.as_bytes());
387        v
388    }
389
390    #[test]
391    fn keepalive() {
392        let m = decode(b(&[b"k", &u64be(0x16B3748), &u64be(123), &[1]])).unwrap();
393        assert_eq!(
394            m,
395            ReplicationMessage::PrimaryKeepalive {
396                wal_end: Lsn(0x16B3748),
397                server_clock: 123,
398                reply_requested: true
399            }
400        );
401        assert!(decode(b(&[b"k", &u64be(1)])).is_err());
402        assert!(decode(b(&[b"x"])).is_err());
403    }
404
405    #[test]
406    fn begin_and_commit() {
407        let m = decode(b(&[
408            b"w",
409            &u64be(10),
410            &u64be(20),
411            &u64be(30),
412            b"B",
413            &u64be(100),
414            &u64be(777),
415            &u32be(42),
416        ]))
417        .unwrap();
418        match m {
419            ReplicationMessage::XLogData {
420                wal_start,
421                wal_end,
422                server_clock,
423                body,
424            } => {
425                assert_eq!((wal_start, wal_end, server_clock), (Lsn(10), Lsn(20), 30));
426                assert_eq!(
427                    body,
428                    LogicalMessage::Begin {
429                        final_lsn: Lsn(100),
430                        commit_ts: 777,
431                        xid: 42
432                    }
433                );
434            }
435            _ => panic!(),
436        }
437        let c = decode_logical(b(&[b"C", &[0], &u64be(100), &u64be(108), &u64be(777)])).unwrap();
438        assert_eq!(
439            c,
440            LogicalMessage::Commit {
441                flags: 0,
442                commit_lsn: Lsn(100),
443                end_lsn: Lsn(108),
444                commit_ts: 777
445            }
446        );
447    }
448
449    #[test]
450    fn relation() {
451        let m = decode_logical(b(&[
452            b"R",
453            &u32be(16385),
454            &cstr("public"),
455            &cstr("users"),
456            b"d",
457            &u16be(2),
458            &[1],
459            &cstr("id"),
460            &u32be(20),
461            &u32be(0xFFFF_FFFF),
462            &[0],
463            &cstr("email"),
464            &u32be(25),
465            &u32be(0xFFFF_FFFF),
466        ]))
467        .unwrap();
468        let LogicalMessage::Relation(r) = m else {
469            panic!()
470        };
471        assert_eq!(r.id, 16385);
472        assert_eq!(r.qualified_name(), "public.users");
473        assert_eq!(r.replica_identity, ReplicaIdentity::Default);
474        assert_eq!(r.columns.len(), 2);
475        assert!(r.columns[0].is_key && !r.columns[1].is_key);
476        assert_eq!(r.columns[0].type_oid, 20);
477        assert_eq!(r.columns[1].type_modifier, -1);
478        assert_eq!(r.key_columns().map(|(i, _)| i).collect::<Vec<_>>(), vec![0]);
479
480        // Empty namespace means pg_catalog.
481        let m = decode_logical(b(&[
482            b"R",
483            &u32be(1),
484            &cstr(""),
485            &cstr("t"),
486            b"n",
487            &u16be(0),
488        ]))
489        .unwrap();
490        let LogicalMessage::Relation(r) = m else {
491            panic!()
492        };
493        assert_eq!(r.schema, "pg_catalog");
494    }
495
496    #[test]
497    fn insert_update_delete_tuples() {
498        let ins = decode_logical(b(&[
499            b"I",
500            &u32be(7),
501            b"N",
502            &u16be(3),
503            &text("1"),
504            b"n",
505            b"u",
506        ]))
507        .unwrap();
508        assert_eq!(
509            ins,
510            LogicalMessage::Insert {
511                relation: 7,
512                new: Tuple {
513                    values: vec![Value::text("1"), Value::Null, Value::Unchanged]
514                }
515            }
516        );
517
518        // Update with old key.
519        let upd = decode_logical(b(&[
520            b"U",
521            &u32be(7),
522            b"K",
523            &u16be(1),
524            &text("1"),
525            b"N",
526            &u16be(1),
527            &text("2"),
528        ]))
529        .unwrap();
530        assert_eq!(
531            upd,
532            LogicalMessage::Update {
533                relation: 7,
534                key: Some(Tuple {
535                    values: vec![Value::text("1")]
536                }),
537                old: None,
538                new: Tuple {
539                    values: vec![Value::text("2")]
540                }
541            }
542        );
543        // Update with full old row.
544        let upd = decode_logical(b(&[
545            b"U",
546            &u32be(7),
547            b"O",
548            &u16be(1),
549            &text("1"),
550            b"N",
551            &u16be(1),
552            &text("2"),
553        ]))
554        .unwrap();
555        assert!(matches!(
556            upd,
557            LogicalMessage::Update {
558                old: Some(_),
559                key: None,
560                ..
561            }
562        ));
563        // Update without old tuple.
564        let upd = decode_logical(b(&[b"U", &u32be(7), b"N", &u16be(1), &text("2")])).unwrap();
565        assert!(matches!(
566            upd,
567            LogicalMessage::Update {
568                old: None,
569                key: None,
570                ..
571            }
572        ));
573
574        let del = decode_logical(b(&[b"D", &u32be(7), b"K", &u16be(1), &text("1")])).unwrap();
575        assert_eq!(
576            del,
577            LogicalMessage::Delete {
578                relation: 7,
579                key: Some(Tuple {
580                    values: vec![Value::text("1")]
581                }),
582                old: None
583            }
584        );
585        let del = decode_logical(b(&[b"D", &u32be(7), b"O", &u16be(1), &text("1")])).unwrap();
586        assert!(matches!(
587            del,
588            LogicalMessage::Delete {
589                old: Some(_),
590                key: None,
591                ..
592            }
593        ));
594        assert!(decode_logical(b(&[b"D", &u32be(7), b"N", &u16be(0)])).is_err());
595    }
596
597    #[test]
598    fn binary_values_and_truncation() {
599        let ins = decode_logical(b(&[
600            b"I",
601            &u32be(7),
602            b"N",
603            &u16be(1),
604            b"b",
605            &u32be(2),
606            &[0xAB, 0xCD],
607        ]))
608        .unwrap();
609        assert_eq!(
610            ins,
611            LogicalMessage::Insert {
612                relation: 7,
613                new: Tuple {
614                    values: vec![Value::Binary(Bytes::from_static(&[0xAB, 0xCD]))]
615                }
616            }
617        );
618        // Declared length longer than payload.
619        assert!(
620            decode_logical(b(&[
621                b"I",
622                &u32be(7),
623                b"N",
624                &u16be(1),
625                b"t",
626                &u32be(5),
627                b"ab"
628            ]))
629            .is_err()
630        );
631        // Unknown value kind.
632        assert!(decode_logical(b(&[b"I", &u32be(7), b"N", &u16be(1), b"z"])).is_err());
633    }
634
635    #[test]
636    fn truncate_origin_type_message_unsupported() {
637        let t = decode_logical(b(&[b"T", &u32be(2), &[3], &u32be(1), &u32be(2)])).unwrap();
638        assert_eq!(
639            t,
640            LogicalMessage::Truncate {
641                relations: vec![1, 2],
642                cascade: true,
643                restart_identity: true
644            }
645        );
646        let o = decode_logical(b(&[b"O", &u64be(5), &cstr("node1")])).unwrap();
647        assert_eq!(
648            o,
649            LogicalMessage::Origin {
650                commit_lsn: Lsn(5),
651                name: "node1".into()
652            }
653        );
654        let y = decode_logical(b(&[b"Y", &u32be(99), &cstr("public"), &cstr("mood")])).unwrap();
655        assert_eq!(
656            y,
657            LogicalMessage::Type {
658                oid: 99,
659                schema: "public".into(),
660                name: "mood".into()
661            }
662        );
663        let m =
664            decode_logical(b(&[b"M", &[1], &u64be(9), &cstr("pfx"), &u32be(3), b"abc"])).unwrap();
665        assert_eq!(
666            m,
667            LogicalMessage::Message {
668                transactional: true,
669                lsn: Lsn(9),
670                prefix: "pfx".into(),
671                content: Bytes::from_static(b"abc")
672            }
673        );
674        for tag in [b'S', b'E', b'c', b'A', b'b', b'P', b'K', b'r', b'p'] {
675            assert_eq!(
676                decode_logical(b(&[&[tag], &[0; 40]])).unwrap(),
677                LogicalMessage::Unsupported { tag }
678            );
679        }
680    }
681
682    #[test]
683    fn standby_status_layout() {
684        let s = encode_standby_status(Lsn(1), Lsn(2), Lsn(3), 4, true);
685        assert_eq!(s[0], b'r');
686        assert_eq!(&s[1..9], &u64be(1)[..]);
687        assert_eq!(&s[9..17], &u64be(2)[..]);
688        assert_eq!(&s[17..25], &u64be(3)[..]);
689        assert_eq!(&s[25..33], &u64be(4)[..]);
690        assert_eq!(s[33], 1);
691    }
692}