use bytes::{Buf, Bytes};
use super::lsn::Lsn;
use crate::error::PgError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TupleColumn {
Null,
UnchangedToast,
Text(String),
Binary(Bytes),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TupleData {
pub columns: Vec<TupleColumn>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReplicaIdentity {
Default,
Nothing,
Full,
Index,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ColumnSpec {
pub key: bool,
pub name: String,
pub type_oid: u32,
pub type_modifier: i32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RelationBody {
pub rel_id: u32,
pub namespace: String,
pub name: String,
pub replica_identity: ReplicaIdentity,
pub columns: Vec<ColumnSpec>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LogicalReplicationMessage {
Begin {
final_lsn: Lsn,
commit_time: i64,
xid: u32,
},
Commit {
flags: u8,
commit_lsn: Lsn,
end_lsn: Lsn,
commit_time: i64,
},
Origin {
commit_lsn: Lsn,
name: String,
},
Relation(RelationBody),
Type {
id: u32,
namespace: String,
name: String,
},
Insert {
rel_id: u32,
new_tuple: TupleData,
},
Update {
rel_id: u32,
old_tuple: Option<TupleData>,
old_tuple_is_full: bool,
new_tuple: TupleData,
},
Delete {
rel_id: u32,
old_tuple: TupleData,
old_tuple_is_full: bool,
},
Truncate {
cascade: bool,
restart_identity: bool,
rel_ids: Vec<u32>,
},
Message {
transactional: bool,
lsn: Lsn,
prefix: String,
content: Bytes,
},
}
pub fn decode_message(payload: &[u8]) -> Result<LogicalReplicationMessage, PgError> {
let mut buf = payload;
let tag = read_u8(&mut buf)?;
match tag {
b'B' => decode_begin(&mut buf),
b'C' => decode_commit(&mut buf),
b'O' => decode_origin(&mut buf),
b'R' => decode_relation(&mut buf),
b'Y' => decode_type(&mut buf),
b'I' => decode_insert(&mut buf),
b'U' => decode_update(&mut buf),
b'D' => decode_delete(&mut buf),
b'T' => decode_truncate(&mut buf),
b'M' => decode_logical_message(&mut buf),
other => Err(protocol_err(format!(
"unknown pgoutput message tag: 0x{other:02X} ({:?})",
other as char
))),
}
}
fn decode_begin(buf: &mut &[u8]) -> Result<LogicalReplicationMessage, PgError> {
let final_lsn = Lsn::from_u64(read_u64(buf)?);
let commit_time = read_i64(buf)?;
let xid = read_u32(buf)?;
Ok(LogicalReplicationMessage::Begin {
final_lsn,
commit_time,
xid,
})
}
fn decode_commit(buf: &mut &[u8]) -> Result<LogicalReplicationMessage, PgError> {
let flags = read_u8(buf)?;
let commit_lsn = Lsn::from_u64(read_u64(buf)?);
let end_lsn = Lsn::from_u64(read_u64(buf)?);
let commit_time = read_i64(buf)?;
Ok(LogicalReplicationMessage::Commit {
flags,
commit_lsn,
end_lsn,
commit_time,
})
}
fn decode_origin(buf: &mut &[u8]) -> Result<LogicalReplicationMessage, PgError> {
let commit_lsn = Lsn::from_u64(read_u64(buf)?);
let name = read_cstring(buf)?;
Ok(LogicalReplicationMessage::Origin { commit_lsn, name })
}
fn decode_relation(buf: &mut &[u8]) -> Result<LogicalReplicationMessage, PgError> {
let rel_id = read_u32(buf)?;
let namespace = read_cstring(buf)?;
let name = read_cstring(buf)?;
let replica_identity_byte = read_u8(buf)?;
let replica_identity = match replica_identity_byte {
b'd' => ReplicaIdentity::Default,
b'n' => ReplicaIdentity::Nothing,
b'f' => ReplicaIdentity::Full,
b'i' => ReplicaIdentity::Index,
other => {
return Err(protocol_err(format!(
"Relation: unknown REPLICA IDENTITY byte: 0x{other:02X} ({:?})",
other as char
)))
}
};
let raw_num_cols = read_i16(buf)?;
let num_cols = non_negative_usize_16(raw_num_cols, "Relation column count")?;
let mut columns = Vec::with_capacity(num_cols);
for _ in 0..num_cols {
let col_flags = read_u8(buf)?;
let key = col_flags & 0x01 != 0;
let col_name = read_cstring(buf)?;
let type_oid = read_u32(buf)?;
let type_modifier = read_i32(buf)?;
columns.push(ColumnSpec {
key,
name: col_name,
type_oid,
type_modifier,
});
}
Ok(LogicalReplicationMessage::Relation(RelationBody {
rel_id,
namespace,
name,
replica_identity,
columns,
}))
}
fn decode_type(buf: &mut &[u8]) -> Result<LogicalReplicationMessage, PgError> {
let id = read_u32(buf)?;
let namespace = read_cstring(buf)?;
let name = read_cstring(buf)?;
Ok(LogicalReplicationMessage::Type {
id,
namespace,
name,
})
}
fn decode_insert(buf: &mut &[u8]) -> Result<LogicalReplicationMessage, PgError> {
let rel_id = read_u32(buf)?;
let marker = read_u8(buf)?;
if marker != b'N' {
return Err(protocol_err(format!(
"Insert: expected 'N' tuple marker, found 0x{marker:02X} ({:?})",
marker as char
)));
}
let new_tuple = decode_tuple_data(buf)?;
Ok(LogicalReplicationMessage::Insert { rel_id, new_tuple })
}
fn decode_update(buf: &mut &[u8]) -> Result<LogicalReplicationMessage, PgError> {
let rel_id = read_u32(buf)?;
let marker = read_u8(buf)?;
let (old_tuple, old_tuple_is_full, new_tuple) = match marker {
b'K' | b'O' => {
let old_tuple = decode_tuple_data(buf)?;
let new_marker = read_u8(buf)?;
if new_marker != b'N' {
return Err(protocol_err(format!(
"Update: expected 'N' new-tuple marker after old-row image, found 0x{new_marker:02X} ({:?})",
new_marker as char
)));
}
let new_tuple = decode_tuple_data(buf)?;
(Some(old_tuple), marker == b'O', new_tuple)
}
b'N' => {
let new_tuple = decode_tuple_data(buf)?;
(None, false, new_tuple)
}
other => {
return Err(protocol_err(format!(
"Update: expected 'K', 'O', or 'N' marker, found 0x{other:02X} ({:?})",
other as char
)))
}
};
Ok(LogicalReplicationMessage::Update {
rel_id,
old_tuple,
old_tuple_is_full,
new_tuple,
})
}
fn decode_delete(buf: &mut &[u8]) -> Result<LogicalReplicationMessage, PgError> {
let rel_id = read_u32(buf)?;
let marker = read_u8(buf)?;
let old_tuple_is_full = match marker {
b'K' => false,
b'O' => true,
other => {
return Err(protocol_err(format!(
"Delete: expected 'K' or 'O' marker, found 0x{other:02X} ({:?})",
other as char
)))
}
};
let old_tuple = decode_tuple_data(buf)?;
Ok(LogicalReplicationMessage::Delete {
rel_id,
old_tuple,
old_tuple_is_full,
})
}
fn decode_truncate(buf: &mut &[u8]) -> Result<LogicalReplicationMessage, PgError> {
let num_rels = read_u32(buf)?;
let opt_flags = read_u8(buf)?;
let cascade = opt_flags & 0x01 != 0;
let restart_identity = opt_flags & 0x02 != 0;
let mut rel_ids = Vec::new();
for _ in 0..num_rels {
rel_ids.push(read_u32(buf)?);
}
Ok(LogicalReplicationMessage::Truncate {
cascade,
restart_identity,
rel_ids,
})
}
fn decode_logical_message(buf: &mut &[u8]) -> Result<LogicalReplicationMessage, PgError> {
let raw_flags = read_u8(buf)?;
let transactional = raw_flags & 0x01 != 0;
let lsn = Lsn::from_u64(read_u64(buf)?);
let prefix = read_cstring(buf)?;
let raw_content_len = read_u32(buf)?;
let content_len = usize::try_from(raw_content_len)
.map_err(|e| protocol_err(format!("Message: content length out of range: {e}")))?;
let content = read_slice(buf, content_len)?;
Ok(LogicalReplicationMessage::Message {
transactional,
lsn,
prefix,
content: Bytes::copy_from_slice(content),
})
}
fn decode_tuple_data(buf: &mut &[u8]) -> Result<TupleData, PgError> {
let raw_num_cols = read_i16(buf)?;
let num_cols = non_negative_usize_16(raw_num_cols, "TupleData column count")?;
let mut columns = Vec::with_capacity(num_cols);
for _ in 0..num_cols {
let tag = read_u8(buf)?;
let column = match tag {
b'n' => TupleColumn::Null,
b'u' => TupleColumn::UnchangedToast,
b't' => {
let raw_len = read_i32(buf)?;
let len = non_negative_usize_32(raw_len, "TupleData Text column length")?;
let raw = read_slice(buf, len)?;
let text = std::str::from_utf8(raw).map_err(|e| {
protocol_err(format!("TupleData: invalid UTF-8 in Text column: {e}"))
})?;
TupleColumn::Text(text.to_string())
}
b'b' => {
let raw_len = read_i32(buf)?;
let len = non_negative_usize_32(raw_len, "TupleData Binary column length")?;
let raw = read_slice(buf, len)?;
TupleColumn::Binary(Bytes::copy_from_slice(raw))
}
other => {
return Err(protocol_err(format!(
"TupleData: unknown column tag: 0x{other:02X} ({:?})",
other as char
)))
}
};
columns.push(column);
}
Ok(TupleData { columns })
}
fn read_u8(buf: &mut &[u8]) -> Result<u8, PgError> {
if !buf.has_remaining() {
return Err(protocol_err(
"truncated message: expected 1 byte, found end of buffer",
));
}
Ok(buf.get_u8())
}
fn read_i16(buf: &mut &[u8]) -> Result<i16, PgError> {
if buf.remaining() < 2 {
return Err(protocol_err(format!(
"truncated message: expected 2 bytes (i16), found {} remaining",
buf.remaining()
)));
}
Ok(buf.get_i16())
}
fn read_i32(buf: &mut &[u8]) -> Result<i32, PgError> {
if buf.remaining() < 4 {
return Err(protocol_err(format!(
"truncated message: expected 4 bytes (i32), found {} remaining",
buf.remaining()
)));
}
Ok(buf.get_i32())
}
fn read_u32(buf: &mut &[u8]) -> Result<u32, PgError> {
if buf.remaining() < 4 {
return Err(protocol_err(format!(
"truncated message: expected 4 bytes (u32), found {} remaining",
buf.remaining()
)));
}
Ok(buf.get_u32())
}
fn read_i64(buf: &mut &[u8]) -> Result<i64, PgError> {
if buf.remaining() < 8 {
return Err(protocol_err(format!(
"truncated message: expected 8 bytes (i64), found {} remaining",
buf.remaining()
)));
}
Ok(buf.get_i64())
}
fn read_u64(buf: &mut &[u8]) -> Result<u64, PgError> {
if buf.remaining() < 8 {
return Err(protocol_err(format!(
"truncated message: expected 8 bytes (u64), found {} remaining",
buf.remaining()
)));
}
Ok(buf.get_u64())
}
fn read_slice<'a>(buf: &mut &'a [u8], len: usize) -> Result<&'a [u8], PgError> {
if buf.len() < len {
return Err(protocol_err(format!(
"truncated message: expected {len} bytes, found {} remaining",
buf.len()
)));
}
let (taken, rest) = buf.split_at(len);
*buf = rest;
Ok(taken)
}
fn read_cstring(buf: &mut &[u8]) -> Result<String, PgError> {
let Some(nul_pos) = buf.iter().position(|&b| b == 0) else {
return Err(protocol_err(
"unterminated string: no NUL terminator before end of buffer",
));
};
let raw = &buf[..nul_pos];
let s = std::str::from_utf8(raw)
.map_err(|e| protocol_err(format!("invalid UTF-8 in string: {e}")))?
.to_string();
*buf = &buf[nul_pos + 1..];
Ok(s)
}
fn non_negative_usize_16(raw: i16, what: &str) -> Result<usize, PgError> {
if raw < 0 {
return Err(protocol_err(format!(
"{what}: negative count/length ({raw})"
)));
}
usize::try_from(raw).map_err(|e| protocol_err(format!("{what}: out of range: {e}")))
}
fn non_negative_usize_32(raw: i32, what: &str) -> Result<usize, PgError> {
if raw < 0 {
return Err(protocol_err(format!(
"{what}: negative count/length ({raw})"
)));
}
usize::try_from(raw).map_err(|e| protocol_err(format!("{what}: out of range: {e}")))
}
fn protocol_err(msg: impl Into<String>) -> PgError {
PgError::Protocol(msg.into())
}
#[cfg(test)]
mod tests {
use bytes::{BufMut, BytesMut};
use super::*;
fn push_cstring(buf: &mut BytesMut, s: &str) {
buf.put_slice(s.as_bytes());
buf.put_u8(0);
}
fn push_tuple_data_2_text(buf: &mut BytesMut, a: &str, b: &str) {
buf.put_i16(2);
for s in [a, b] {
buf.put_u8(b't');
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
buf.put_i32(s.len() as i32);
buf.put_slice(s.as_bytes());
}
}
#[test]
fn decode_begin_message() {
let mut buf = BytesMut::new();
buf.put_u8(b'B');
buf.put_u64(0x0000_0016_B374_D848);
buf.put_i64(123_456_789_012);
buf.put_u32(4242);
let msg = decode_message(&buf).unwrap();
assert_eq!(
msg,
LogicalReplicationMessage::Begin {
final_lsn: Lsn::from_u64(0x0000_0016_B374_D848),
commit_time: 123_456_789_012,
xid: 4242,
}
);
}
#[test]
fn decode_commit_message() {
let mut buf = BytesMut::new();
buf.put_u8(b'C');
buf.put_u8(0);
buf.put_u64(0x1000);
buf.put_u64(0x2000);
buf.put_i64(999);
let msg = decode_message(&buf).unwrap();
assert_eq!(
msg,
LogicalReplicationMessage::Commit {
flags: 0,
commit_lsn: Lsn::from_u64(0x1000),
end_lsn: Lsn::from_u64(0x2000),
commit_time: 999,
}
);
}
#[test]
fn decode_origin_message() {
let mut buf = BytesMut::new();
buf.put_u8(b'O');
buf.put_u64(0xABCD);
push_cstring(&mut buf, "my_origin");
let msg = decode_message(&buf).unwrap();
assert_eq!(
msg,
LogicalReplicationMessage::Origin {
commit_lsn: Lsn::from_u64(0xABCD),
name: "my_origin".to_string(),
}
);
}
#[test]
fn decode_type_message() {
let mut buf = BytesMut::new();
buf.put_u8(b'Y');
buf.put_u32(16384);
push_cstring(&mut buf, "public");
push_cstring(&mut buf, "my_enum");
let msg = decode_message(&buf).unwrap();
assert_eq!(
msg,
LogicalReplicationMessage::Type {
id: 16384,
namespace: "public".to_string(),
name: "my_enum".to_string(),
}
);
}
#[test]
fn decode_relation_zero_columns() {
let mut buf = BytesMut::new();
buf.put_u8(b'R');
buf.put_u32(16385);
push_cstring(&mut buf, "public");
push_cstring(&mut buf, "empty_table");
buf.put_u8(b'd');
buf.put_i16(0);
let msg = decode_message(&buf).unwrap();
assert_eq!(
msg,
LogicalReplicationMessage::Relation(RelationBody {
rel_id: 16385,
namespace: "public".to_string(),
name: "empty_table".to_string(),
replica_identity: ReplicaIdentity::Default,
columns: vec![],
})
);
}
#[test]
fn decode_relation_three_columns_mixed_keys() {
let mut buf = BytesMut::new();
buf.put_u8(b'R');
buf.put_u32(16400);
push_cstring(&mut buf, "app");
push_cstring(&mut buf, "users");
buf.put_u8(b'i');
buf.put_i16(3);
buf.put_u8(0x01);
push_cstring(&mut buf, "id");
buf.put_u32(23);
buf.put_i32(-1);
buf.put_u8(0x00);
push_cstring(&mut buf, "name");
buf.put_u32(25);
buf.put_i32(-1);
buf.put_u8(0x01);
push_cstring(&mut buf, "tenant_id");
buf.put_u32(23);
buf.put_i32(-1);
let msg = decode_message(&buf).unwrap();
assert_eq!(
msg,
LogicalReplicationMessage::Relation(RelationBody {
rel_id: 16400,
namespace: "app".to_string(),
name: "users".to_string(),
replica_identity: ReplicaIdentity::Index,
columns: vec![
ColumnSpec {
key: true,
name: "id".to_string(),
type_oid: 23,
type_modifier: -1,
},
ColumnSpec {
key: false,
name: "name".to_string(),
type_oid: 25,
type_modifier: -1,
},
ColumnSpec {
key: true,
name: "tenant_id".to_string(),
type_oid: 23,
type_modifier: -1,
},
],
})
);
}
#[test]
fn decode_relation_replica_identity_nothing() {
let mut buf = BytesMut::new();
buf.put_u8(b'R');
buf.put_u32(1);
push_cstring(&mut buf, "s");
push_cstring(&mut buf, "t");
buf.put_u8(b'n');
buf.put_i16(0);
let msg = decode_message(&buf).unwrap();
let LogicalReplicationMessage::Relation(body) = msg else {
panic!("expected Relation, got {msg:?}");
};
assert_eq!(body.replica_identity, ReplicaIdentity::Nothing);
}
#[test]
fn decode_relation_replica_identity_full() {
let mut buf = BytesMut::new();
buf.put_u8(b'R');
buf.put_u32(1);
push_cstring(&mut buf, "s");
push_cstring(&mut buf, "t");
buf.put_u8(b'f');
buf.put_i16(0);
let msg = decode_message(&buf).unwrap();
let LogicalReplicationMessage::Relation(body) = msg else {
panic!("expected Relation, got {msg:?}");
};
assert_eq!(body.replica_identity, ReplicaIdentity::Full);
}
#[test]
fn decode_insert_message() {
let mut buf = BytesMut::new();
buf.put_u8(b'I');
buf.put_u32(16400);
buf.put_u8(b'N');
push_tuple_data_2_text(&mut buf, "42", "alice");
let msg = decode_message(&buf).unwrap();
assert_eq!(
msg,
LogicalReplicationMessage::Insert {
rel_id: 16400,
new_tuple: TupleData {
columns: vec![
TupleColumn::Text("42".to_string()),
TupleColumn::Text("alice".to_string()),
],
},
}
);
}
#[test]
fn decode_insert_tuple_data_all_tag_kinds() {
let mut buf = BytesMut::new();
buf.put_u8(b'I');
buf.put_u32(1);
buf.put_u8(b'N');
buf.put_i16(4);
buf.put_u8(b'n'); buf.put_u8(b'u'); buf.put_u8(b't');
buf.put_i32(5);
buf.put_slice(b"hello");
buf.put_u8(b'b');
buf.put_i32(3);
buf.put_slice(&[0xDE, 0xAD, 0xBE]);
let msg = decode_message(&buf).unwrap();
let LogicalReplicationMessage::Insert { new_tuple, .. } = msg else {
panic!("expected Insert, got {msg:?}");
};
assert_eq!(new_tuple.columns.len(), 4);
assert_eq!(new_tuple.columns[0], TupleColumn::Null);
assert_eq!(new_tuple.columns[1], TupleColumn::UnchangedToast);
assert_ne!(new_tuple.columns[0], new_tuple.columns[1]);
assert_eq!(new_tuple.columns[2], TupleColumn::Text("hello".to_string()));
assert_eq!(
new_tuple.columns[3],
TupleColumn::Binary(Bytes::copy_from_slice(&[0xDE, 0xAD, 0xBE]))
);
}
#[test]
fn decode_update_no_old_tuple() {
let mut buf = BytesMut::new();
buf.put_u8(b'U');
buf.put_u32(1);
buf.put_u8(b'N');
push_tuple_data_2_text(&mut buf, "1", "new_value");
let msg = decode_message(&buf).unwrap();
assert_eq!(
msg,
LogicalReplicationMessage::Update {
rel_id: 1,
old_tuple: None,
old_tuple_is_full: false,
new_tuple: TupleData {
columns: vec![
TupleColumn::Text("1".to_string()),
TupleColumn::Text("new_value".to_string()),
],
},
}
);
}
#[test]
fn decode_update_key_only_old_tuple() {
let mut buf = BytesMut::new();
buf.put_u8(b'U');
buf.put_u32(1);
buf.put_u8(b'K');
push_tuple_data_2_text(&mut buf, "1", "old_key_value");
buf.put_u8(b'N');
push_tuple_data_2_text(&mut buf, "1", "new_value");
let msg = decode_message(&buf).unwrap();
assert_eq!(
msg,
LogicalReplicationMessage::Update {
rel_id: 1,
old_tuple: Some(TupleData {
columns: vec![
TupleColumn::Text("1".to_string()),
TupleColumn::Text("old_key_value".to_string()),
],
}),
old_tuple_is_full: false,
new_tuple: TupleData {
columns: vec![
TupleColumn::Text("1".to_string()),
TupleColumn::Text("new_value".to_string()),
],
},
}
);
}
#[test]
fn decode_update_full_old_tuple() {
let mut buf = BytesMut::new();
buf.put_u8(b'U');
buf.put_u32(1);
buf.put_u8(b'O');
push_tuple_data_2_text(&mut buf, "1", "old_full_value");
buf.put_u8(b'N');
push_tuple_data_2_text(&mut buf, "1", "new_value");
let msg = decode_message(&buf).unwrap();
assert_eq!(
msg,
LogicalReplicationMessage::Update {
rel_id: 1,
old_tuple: Some(TupleData {
columns: vec![
TupleColumn::Text("1".to_string()),
TupleColumn::Text("old_full_value".to_string()),
],
}),
old_tuple_is_full: true,
new_tuple: TupleData {
columns: vec![
TupleColumn::Text("1".to_string()),
TupleColumn::Text("new_value".to_string()),
],
},
}
);
}
#[test]
fn decode_delete_key_only() {
let mut buf = BytesMut::new();
buf.put_u8(b'D');
buf.put_u32(1);
buf.put_u8(b'K');
push_tuple_data_2_text(&mut buf, "1", "x");
let msg = decode_message(&buf).unwrap();
assert_eq!(
msg,
LogicalReplicationMessage::Delete {
rel_id: 1,
old_tuple: TupleData {
columns: vec![
TupleColumn::Text("1".to_string()),
TupleColumn::Text("x".to_string()),
],
},
old_tuple_is_full: false,
}
);
}
#[test]
fn decode_delete_full() {
let mut buf = BytesMut::new();
buf.put_u8(b'D');
buf.put_u32(1);
buf.put_u8(b'O');
push_tuple_data_2_text(&mut buf, "1", "x");
let msg = decode_message(&buf).unwrap();
assert_eq!(
msg,
LogicalReplicationMessage::Delete {
rel_id: 1,
old_tuple: TupleData {
columns: vec![
TupleColumn::Text("1".to_string()),
TupleColumn::Text("x".to_string()),
],
},
old_tuple_is_full: true,
}
);
}
#[test]
fn decode_truncate_no_flags_single_rel() {
let mut buf = BytesMut::new();
buf.put_u8(b'T');
buf.put_u32(1);
buf.put_u8(0x00);
buf.put_u32(100);
let msg = decode_message(&buf).unwrap();
assert_eq!(
msg,
LogicalReplicationMessage::Truncate {
cascade: false,
restart_identity: false,
rel_ids: vec![100],
}
);
}
#[test]
fn decode_truncate_cascade_only() {
let mut buf = BytesMut::new();
buf.put_u8(b'T');
buf.put_u32(1);
buf.put_u8(0x01);
buf.put_u32(200);
let msg = decode_message(&buf).unwrap();
assert_eq!(
msg,
LogicalReplicationMessage::Truncate {
cascade: true,
restart_identity: false,
rel_ids: vec![200],
}
);
}
#[test]
fn decode_truncate_restart_identity_only() {
let mut buf = BytesMut::new();
buf.put_u8(b'T');
buf.put_u32(1);
buf.put_u8(0x02);
buf.put_u32(300);
let msg = decode_message(&buf).unwrap();
assert_eq!(
msg,
LogicalReplicationMessage::Truncate {
cascade: false,
restart_identity: true,
rel_ids: vec![300],
}
);
}
#[test]
fn decode_truncate_both_flags_multiple_rels() {
let mut buf = BytesMut::new();
buf.put_u8(b'T');
buf.put_u32(3);
buf.put_u8(0x03);
buf.put_u32(10);
buf.put_u32(20);
buf.put_u32(30);
let msg = decode_message(&buf).unwrap();
assert_eq!(
msg,
LogicalReplicationMessage::Truncate {
cascade: true,
restart_identity: true,
rel_ids: vec![10, 20, 30],
}
);
}
#[test]
fn decode_logical_message_transactional() {
let mut buf = BytesMut::new();
buf.put_u8(b'M');
buf.put_u8(0x01);
buf.put_u64(0x5000);
push_cstring(&mut buf, "my_app");
let content = b"hello world";
buf.put_u32(u32::try_from(content.len()).unwrap());
buf.put_slice(content);
let msg = decode_message(&buf).unwrap();
assert_eq!(
msg,
LogicalReplicationMessage::Message {
transactional: true,
lsn: Lsn::from_u64(0x5000),
prefix: "my_app".to_string(),
content: Bytes::copy_from_slice(content),
}
);
}
#[test]
fn decode_logical_message_non_transactional() {
let mut buf = BytesMut::new();
buf.put_u8(b'M');
buf.put_u8(0x00);
buf.put_u64(0x6000);
push_cstring(&mut buf, "my_app");
let content = b"non-tx";
buf.put_u32(u32::try_from(content.len()).unwrap());
buf.put_slice(content);
let msg = decode_message(&buf).unwrap();
assert_eq!(
msg,
LogicalReplicationMessage::Message {
transactional: false,
lsn: Lsn::from_u64(0x6000),
prefix: "my_app".to_string(),
content: Bytes::copy_from_slice(content),
}
);
}
#[test]
fn decode_logical_message_empty_content() {
let mut buf = BytesMut::new();
buf.put_u8(b'M');
buf.put_u8(0x01);
buf.put_u64(0x7000);
push_cstring(&mut buf, "empty");
buf.put_u32(0);
let msg = decode_message(&buf).unwrap();
assert_eq!(
msg,
LogicalReplicationMessage::Message {
transactional: true,
lsn: Lsn::from_u64(0x7000),
prefix: "empty".to_string(),
content: Bytes::new(),
}
);
}
#[test]
fn decode_err_unknown_top_level_tag() {
let buf = [b'Z', 0, 0, 0, 0];
let err = decode_message(&buf).unwrap_err();
assert!(matches!(err, PgError::Protocol(_)));
}
#[test]
fn decode_err_unknown_replica_identity_byte() {
let mut buf = BytesMut::new();
buf.put_u8(b'R');
buf.put_u32(1);
push_cstring(&mut buf, "s");
push_cstring(&mut buf, "t");
buf.put_u8(b'x'); buf.put_i16(0);
let err = decode_message(&buf).unwrap_err();
assert!(matches!(err, PgError::Protocol(_)));
}
#[test]
fn decode_err_unknown_tuple_data_tag() {
let mut buf = BytesMut::new();
buf.put_u8(b'I');
buf.put_u32(1);
buf.put_u8(b'N');
buf.put_i16(1);
buf.put_u8(b'z');
let err = decode_message(&buf).unwrap_err();
assert!(matches!(err, PgError::Protocol(_)));
}
#[test]
fn decode_err_empty_payload() {
let err = decode_message(&[]).unwrap_err();
assert!(matches!(err, PgError::Protocol(_)));
}
#[test]
fn decode_err_cstring_missing_terminator() {
let mut buf = BytesMut::new();
buf.put_u8(b'O');
buf.put_u64(1);
buf.put_slice(b"no_terminator");
let err = decode_message(&buf).unwrap_err();
assert!(matches!(err, PgError::Protocol(_)));
}
#[test]
fn decode_err_truncated_mid_integer() {
let mut buf = BytesMut::new();
buf.put_u8(b'B');
buf.put_slice(&[0, 0, 0]);
let err = decode_message(&buf).unwrap_err();
assert!(matches!(err, PgError::Protocol(_)));
}
#[test]
fn decode_err_negative_tuple_column_length() {
let mut buf = BytesMut::new();
buf.put_u8(b'I');
buf.put_u32(1);
buf.put_u8(b'N');
buf.put_i16(1);
buf.put_u8(b't');
buf.put_i32(-1);
let err = decode_message(&buf).unwrap_err();
assert!(matches!(err, PgError::Protocol(_)));
}
#[test]
fn decode_err_update_invalid_marker_byte() {
let mut buf = BytesMut::new();
buf.put_u8(b'U');
buf.put_u32(1);
buf.put_u8(b'X');
let err = decode_message(&buf).unwrap_err();
assert!(matches!(err, PgError::Protocol(_)));
}
#[test]
fn decode_err_insert_wrong_tuple_marker() {
let mut buf = BytesMut::new();
buf.put_u8(b'I');
buf.put_u32(1);
buf.put_u8(b'X');
let err = decode_message(&buf).unwrap_err();
assert!(matches!(err, PgError::Protocol(_)));
}
#[test]
fn decode_err_message_content_length_exceeds_buffer() {
let mut buf = BytesMut::new();
buf.put_u8(b'M');
buf.put_u8(0x00);
buf.put_u64(1);
push_cstring(&mut buf, "p");
buf.put_u32(1000);
let err = decode_message(&buf).unwrap_err();
assert!(matches!(err, PgError::Protocol(_)));
}
#[test]
fn decode_err_truncate_rel_ids_cut_short() {
let mut buf = BytesMut::new();
buf.put_u8(b'T');
buf.put_u32(5); buf.put_u8(0x00);
buf.put_u32(1);
let err = decode_message(&buf).unwrap_err();
assert!(matches!(err, PgError::Protocol(_)));
}
}