1use std::collections::{BTreeMap, HashMap};
2
3use crate::{
4 PgLsn, PostgresConnectError,
5 ids::{CommitLsn, EndLsn, RelationId, TransactionId},
6 model::{
7 ColumnSchema, ColumnValue, EntityDifference, EntityKey, Operation, ScalarValue, TableSchema,
8 },
9};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct RelationEvent {
13 pub relation_id: RelationId,
14 pub schema: String,
15 pub table: String,
16 pub columns: Vec<ColumnSchema>,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub enum RowTupleKind {
21 Full,
22 Key,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, Hash)]
26pub enum RowEventKind {
27 Insert,
28 Update {
29 old: Vec<ColumnValue>,
30 old_tuple_kind: RowTupleKind,
31 },
32 Delete {
33 tuple_kind: RowTupleKind,
34 },
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, Hash)]
38pub struct RowEvent {
39 pub relation_id: RelationId,
40 pub lsn: PgLsn,
41 pub commit_lsn: Option<PgLsn>,
42 pub txid: Option<TransactionId>,
43 pub commit_timestamp_ms: Option<i64>,
44 pub kind: RowEventKind,
45 pub values: Vec<ColumnValue>,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub enum DecodedMessage {
50 Begin {
51 final_lsn: PgLsn,
52 xid: TransactionId,
53 },
54 Commit {
55 commit_lsn: CommitLsn,
56 end_lsn: EndLsn,
57 commit_timestamp_ms: i64,
58 },
59 Relation(RelationEvent),
60 Row(RowEvent),
61 Keepalive,
62}
63
64pub fn decode_pgoutput_message(
67 bytes: &[u8],
68 lsn: PgLsn,
69 txid: Option<TransactionId>,
70) -> Result<DecodedMessage, PostgresConnectError> {
71 let mut reader = PgOutputReader::new(bytes);
72 let tag = reader.read_u8("message tag")?;
73
74 let message = match tag {
75 b'B' => {
76 let final_lsn = PgLsn(reader.read_u64("begin final_lsn")?);
77 let _commit_time = reader.read_i64("begin commit_time")?;
78 let xid = TransactionId(i64::from(reader.read_u32("begin xid")?));
79 DecodedMessage::Begin { final_lsn, xid }
80 }
81 b'C' => {
82 let _flags = reader.read_u8("commit flags")?;
83 let commit_lsn = CommitLsn(PgLsn(reader.read_u64("commit commit_lsn")?));
84 let end_lsn = EndLsn(PgLsn(reader.read_u64("commit end_lsn")?));
85 let commit_timestamp_ms =
86 postgres_timestamp_micros_to_unix_ms(reader.read_i64("commit commit_time")?);
87 DecodedMessage::Commit {
88 commit_lsn,
89 end_lsn,
90 commit_timestamp_ms,
91 }
92 }
93 b'R' => DecodedMessage::Relation(decode_relation(&mut reader)?),
94 b'I' => DecodedMessage::Row(decode_insert(&mut reader, lsn, txid)?),
95 b'U' => DecodedMessage::Row(decode_update(&mut reader, lsn, txid)?),
96 b'D' => DecodedMessage::Row(decode_delete(&mut reader, lsn, txid)?),
97 tag => {
98 return Err(PostgresConnectError::Backend(format!(
99 "unsupported pgoutput message tag {:?}",
100 char::from(tag)
101 )));
102 }
103 };
104
105 reader.finish()?;
106 Ok(message)
107}
108
109fn decode_relation(reader: &mut PgOutputReader<'_>) -> Result<RelationEvent, PostgresConnectError> {
110 let relation_id = RelationId(reader.read_u32("relation id")?);
111 let schema = reader.read_cstr("relation namespace")?;
112 let table = reader.read_cstr("relation name")?;
113 let _replica_identity = reader.read_u8("relation replica identity")?;
114 let column_count = reader.read_count("relation column count")?;
115 let mut columns = Vec::with_capacity(column_count);
116
117 for index in 0..column_count {
118 let flags = reader.read_u8("relation column flags")?;
119 let name = reader.read_cstr("relation column name")?;
120 let type_oid = reader.read_i32("relation column type oid")?;
121 let _atttypmod = reader.read_i32("relation column atttypmod")?;
122 columns.push(ColumnSchema {
123 name,
124 type_name: type_name(type_oid).to_owned(),
125 key: flags & 1 == 1,
126 });
127
128 if columns[index].name.is_empty() {
129 return Err(PostgresConnectError::Backend(format!(
130 "invalid pgoutput relation column {index}: empty name"
131 )));
132 }
133 }
134
135 Ok(RelationEvent {
136 relation_id,
137 schema,
138 table,
139 columns,
140 })
141}
142
143fn decode_insert(
144 reader: &mut PgOutputReader<'_>,
145 lsn: PgLsn,
146 txid: Option<TransactionId>,
147) -> Result<RowEvent, PostgresConnectError> {
148 let relation_id = RelationId(reader.read_u32("insert relation id")?);
149 reader.expect_u8(b'N', "insert tuple tag N")?;
150 let values = decode_tuple(reader)?;
151
152 Ok(RowEvent {
153 relation_id,
154 lsn,
155 commit_lsn: None,
156 txid,
157 commit_timestamp_ms: None,
158 kind: RowEventKind::Insert,
159 values,
160 })
161}
162
163fn decode_update(
164 reader: &mut PgOutputReader<'_>,
165 lsn: PgLsn,
166 txid: Option<TransactionId>,
167) -> Result<RowEvent, PostgresConnectError> {
168 let relation_id = RelationId(reader.read_u32("update relation id")?);
169 let tag = reader.read_u8("update tuple tag")?;
170 let (old, old_tuple_kind, new_tag) = match tag {
171 b'K' | b'O' => (
172 decode_tuple(reader)?,
173 tuple_kind(tag),
174 reader.read_u8("update new tuple tag")?,
175 ),
176 b'N' => (Vec::new(), RowTupleKind::Full, b'N'),
177 tag => {
178 return Err(PostgresConnectError::Backend(format!(
179 "invalid pgoutput update tuple tag {:?}",
180 char::from(tag)
181 )));
182 }
183 };
184
185 if new_tag != b'N' {
186 return Err(PostgresConnectError::Backend(format!(
187 "invalid pgoutput update new tuple tag {:?}",
188 char::from(new_tag)
189 )));
190 }
191
192 let values = decode_tuple(reader)?;
193 Ok(RowEvent {
194 relation_id,
195 lsn,
196 commit_lsn: None,
197 txid,
198 commit_timestamp_ms: None,
199 kind: RowEventKind::Update {
200 old,
201 old_tuple_kind,
202 },
203 values,
204 })
205}
206
207fn decode_delete(
208 reader: &mut PgOutputReader<'_>,
209 lsn: PgLsn,
210 txid: Option<TransactionId>,
211) -> Result<RowEvent, PostgresConnectError> {
212 let relation_id = RelationId(reader.read_u32("delete relation id")?);
213 let tag = reader.read_u8("delete tuple tag")?;
214 if !matches!(tag, b'K' | b'O') {
215 return Err(PostgresConnectError::Backend(format!(
216 "invalid pgoutput delete tuple tag {:?}",
217 char::from(tag)
218 )));
219 }
220 let values = decode_tuple(reader)?;
221
222 Ok(RowEvent {
223 relation_id,
224 lsn,
225 commit_lsn: None,
226 txid,
227 commit_timestamp_ms: None,
228 kind: RowEventKind::Delete {
229 tuple_kind: tuple_kind(tag),
230 },
231 values,
232 })
233}
234
235fn tuple_kind(tag: u8) -> RowTupleKind {
236 if tag == b'K' {
237 RowTupleKind::Key
238 } else {
239 RowTupleKind::Full
240 }
241}
242
243fn decode_tuple(reader: &mut PgOutputReader<'_>) -> Result<Vec<ColumnValue>, PostgresConnectError> {
244 let value_count = reader.read_count("tuple value count")?;
245 let mut values = Vec::with_capacity(value_count);
246
247 for index in 0..value_count {
248 let value_tag = reader.read_u8("tuple value tag")?;
249 let value = match value_tag {
250 b'n' => ScalarValue::Null,
251 b'u' => ScalarValue::UnchangedToast,
252 b't' => {
253 let bytes = reader.read_len_bytes("text tuple value")?;
254 let value = std::str::from_utf8(bytes).map_err(|error| {
255 PostgresConnectError::Backend(format!(
256 "invalid utf8 in pgoutput text tuple value: {error}"
257 ))
258 })?;
259 ScalarValue::Text(value.to_owned())
260 }
261 b'b' => ScalarValue::Bytes(reader.read_len_bytes("binary tuple value")?.to_vec()),
262 tag => {
263 return Err(PostgresConnectError::Backend(format!(
264 "unsupported pgoutput tuple value tag {:?}",
265 char::from(tag)
266 )));
267 }
268 };
269
270 values.push(ColumnValue {
271 name: format!("col{index}"),
272 value,
273 });
274 }
275
276 Ok(values)
277}
278
279fn type_name(type_oid: i32) -> &'static str {
280 match type_oid {
281 16 => "bool",
282 17 => "bytea",
283 20 => "int8",
284 21 => "int2",
285 23 => "int4",
286 25 => "text",
287 700 => "float4",
288 701 => "float8",
289 1043 => "varchar",
290 1700 => "numeric",
291 _ => "unknown",
292 }
293}
294
295fn postgres_timestamp_micros_to_unix_ms(value: i64) -> i64 {
296 const POSTGRES_EPOCH_UNIX_MS: i64 = 946_684_800_000;
297 POSTGRES_EPOCH_UNIX_MS + value.div_euclid(1_000)
298}
299
300struct PgOutputReader<'a> {
301 bytes: &'a [u8],
302 position: usize,
303}
304
305impl<'a> PgOutputReader<'a> {
306 fn new(bytes: &'a [u8]) -> Self {
307 Self { bytes, position: 0 }
308 }
309
310 fn finish(&self) -> Result<(), PostgresConnectError> {
311 if self.position == self.bytes.len() {
312 Ok(())
313 } else {
314 Err(PostgresConnectError::Backend(format!(
315 "invalid pgoutput message: {} trailing bytes",
316 self.bytes.len() - self.position
317 )))
318 }
319 }
320
321 fn read_u8(&mut self, field: &str) -> Result<u8, PostgresConnectError> {
322 let bytes = self.read_exact(1, field)?;
323 Ok(bytes[0])
324 }
325
326 fn expect_u8(&mut self, expected: u8, field: &str) -> Result<(), PostgresConnectError> {
327 let actual = self.read_u8(field)?;
328 if actual == expected {
329 Ok(())
330 } else {
331 Err(PostgresConnectError::Backend(format!(
332 "invalid pgoutput {field}: expected {:?}, got {:?}",
333 char::from(expected),
334 char::from(actual)
335 )))
336 }
337 }
338
339 fn read_i16(&mut self, field: &str) -> Result<i16, PostgresConnectError> {
340 let bytes = self.read_exact(2, field)?;
341 Ok(i16::from_be_bytes([bytes[0], bytes[1]]))
342 }
343
344 fn read_count(&mut self, field: &str) -> Result<usize, PostgresConnectError> {
345 let count = self.read_i16(field)?;
346 usize::try_from(count).map_err(|_| {
347 PostgresConnectError::Backend(format!("invalid pgoutput {field}: negative count"))
348 })
349 }
350
351 fn read_i32(&mut self, field: &str) -> Result<i32, PostgresConnectError> {
352 let bytes = self.read_exact(4, field)?;
353 Ok(i32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
354 }
355
356 fn read_u32(&mut self, field: &str) -> Result<u32, PostgresConnectError> {
357 let bytes = self.read_exact(4, field)?;
358 Ok(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
359 }
360
361 fn read_i64(&mut self, field: &str) -> Result<i64, PostgresConnectError> {
362 let bytes = self.read_exact(8, field)?;
363 Ok(i64::from_be_bytes([
364 bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
365 ]))
366 }
367
368 fn read_u64(&mut self, field: &str) -> Result<u64, PostgresConnectError> {
369 let bytes = self.read_exact(8, field)?;
370 Ok(u64::from_be_bytes([
371 bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
372 ]))
373 }
374
375 fn read_len_bytes(&mut self, field: &str) -> Result<&'a [u8], PostgresConnectError> {
376 let length = self.read_i32(field)?;
377 let length = usize::try_from(length).map_err(|_| {
378 PostgresConnectError::Backend(format!("invalid pgoutput {field}: negative length"))
379 })?;
380 self.read_exact(length, field)
381 }
382
383 fn read_cstr(&mut self, field: &str) -> Result<String, PostgresConnectError> {
384 let remaining = &self.bytes[self.position..];
385 let Some(length) = remaining.iter().position(|byte| *byte == 0) else {
386 return Err(PostgresConnectError::Backend(format!(
387 "truncated pgoutput {field}: missing null terminator"
388 )));
389 };
390 let bytes = &remaining[..length];
391 self.position += length + 1;
392 let value = std::str::from_utf8(bytes).map_err(|error| {
393 PostgresConnectError::Backend(format!("invalid utf8 in pgoutput {field}: {error}"))
394 })?;
395 Ok(value.to_owned())
396 }
397
398 fn read_exact(&mut self, length: usize, field: &str) -> Result<&'a [u8], PostgresConnectError> {
399 let end = self.position.checked_add(length).ok_or_else(|| {
400 PostgresConnectError::Backend(format!("invalid pgoutput {field}: length overflow"))
401 })?;
402 if end > self.bytes.len() {
403 return Err(PostgresConnectError::Backend(format!(
404 "truncated pgoutput {field}: needed {length} bytes, have {}",
405 self.bytes.len().saturating_sub(self.position)
406 )));
407 }
408
409 let bytes = &self.bytes[self.position..end];
410 self.position = end;
411 Ok(bytes)
412 }
413}
414
415#[derive(Debug, Clone, Default)]
416pub struct RelationCache {
417 relations: BTreeMap<RelationId, CachedRelation>,
418}
419
420#[derive(Debug, Clone)]
421struct CachedRelation {
422 schema: TableSchema,
423 column_indices: HashMap<String, usize>,
425}
426
427impl RelationCache {
428 pub fn apply_relation(&mut self, event: RelationEvent) {
429 let mut column_indices = HashMap::with_capacity(event.columns.len());
430 for (index, column) in event.columns.iter().enumerate() {
431 column_indices.entry(column.name.clone()).or_insert(index);
432 }
433 self.relations.insert(
434 event.relation_id,
435 CachedRelation {
436 schema: TableSchema {
437 schema: event.schema,
438 table: event.table,
439 columns: event.columns,
440 },
441 column_indices,
442 },
443 );
444 }
445
446 pub fn translate(&self, event: RowEvent) -> Result<EntityDifference, PostgresConnectError> {
449 let relation = self.relations.get(&event.relation_id).ok_or_else(|| {
450 PostgresConnectError::Backend(format!(
451 "missing relation metadata for relation id {}",
452 event.relation_id
453 ))
454 })?;
455 let schema = &relation.schema;
456 let table = format!("{}.{}", schema.schema, schema.table);
457 let lsn = event.commit_lsn.unwrap_or(event.lsn);
458
459 let (op, before, after, key_columns) = match event.kind {
460 RowEventKind::Insert => {
461 let values = normalize_values(relation, event.values, "row values")?;
462 let key_columns = extract_key_columns(schema, &values)?;
463 (Operation::Insert, Vec::new(), values, key_columns)
464 }
465 RowEventKind::Update { old, .. } => {
466 let values = normalize_values(relation, event.values, "row values")?;
467 let key_columns = extract_key_columns(schema, &values)?;
468 let old = if old.is_empty() {
469 old
470 } else {
471 normalize_values(relation, old, "old row values")?
472 };
473 if has_any_key_column(schema, &old) {
474 let old_key_columns = extract_key_columns(schema, &old)?;
475 if old_key_columns != key_columns {
476 return Err(PostgresConnectError::Backend(
477 "key-changing updates are not supported".to_owned(),
478 ));
479 }
480 }
481
482 (Operation::Update, old, values, key_columns)
483 }
484 RowEventKind::Delete { .. } => {
485 let values = normalize_values(relation, event.values, "row values")?;
486 let key_columns = extract_key_columns(schema, &values)?;
487 (Operation::Delete, values, Vec::new(), key_columns)
488 }
489 };
490 let key = EntityKey {
491 table: table.clone(),
492 columns: key_columns,
493 };
494
495 Ok(EntityDifference {
496 table,
497 key,
498 op,
499 before,
500 after,
501 lsn,
502 txid: event.txid,
503 commit_timestamp_ms: event.commit_timestamp_ms,
504 schema: schema.clone(),
505 })
506 }
507}
508
509fn normalize_values(
510 relation: &CachedRelation,
511 values: Vec<ColumnValue>,
512 label: &str,
513) -> Result<Vec<ColumnValue>, PostgresConnectError> {
514 let schema = &relation.schema;
518
519 if values.len() != schema.columns.len() {
520 return Err(PostgresConnectError::Backend(format!(
521 "column count mismatch for {}.{} {label}: decoded {} values but expected {} columns",
522 schema.schema,
523 schema.table,
524 values.len(),
525 schema.columns.len()
526 )));
527 }
528
529 if values
532 .iter()
533 .all(|value| relation.column_indices.contains_key(&value.name))
534 {
535 return values
536 .into_iter()
537 .map(|value| {
538 let column = &schema.columns[relation.column_indices[&value.name]];
539 Ok(ColumnValue {
540 name: value.name,
541 value: coerce_value(value.value, &column.type_name)?,
542 })
543 })
544 .collect();
545 }
546
547 values
548 .into_iter()
549 .zip(&schema.columns)
550 .map(|(value, column)| {
551 Ok(ColumnValue {
552 name: column.name.clone(),
553 value: coerce_value(value.value, &column.type_name)?,
554 })
555 })
556 .collect::<Result<Vec<_>, _>>()
557}
558
559fn coerce_value(value: ScalarValue, type_name: &str) -> Result<ScalarValue, PostgresConnectError> {
560 let ScalarValue::Text(text) = value else {
561 return Ok(value);
562 };
563
564 match type_name {
565 "bool" => coerce_bool(&text),
566 "int2" | "int4" | "int8" => text.parse::<i64>().map(ScalarValue::Int).map_err(|error| {
567 PostgresConnectError::Backend(format!(
568 "invalid {type_name} pgoutput text value {text:?}: {error}"
569 ))
570 }),
571 "float4" | "float8" | "numeric" => Ok(ScalarValue::Float(text)),
572 "bytea" => Ok(ScalarValue::Bytes(coerce_bytea(&text)?)),
573 _ => Ok(ScalarValue::Text(text)),
574 }
575}
576
577fn coerce_bool(text: &str) -> Result<ScalarValue, PostgresConnectError> {
578 match text {
579 "t" | "true" | "1" => Ok(ScalarValue::Bool(true)),
580 "f" | "false" | "0" => Ok(ScalarValue::Bool(false)),
581 _ => Err(PostgresConnectError::Backend(format!(
582 "invalid bool pgoutput text value {text:?}"
583 ))),
584 }
585}
586
587fn coerce_bytea(text: &str) -> Result<Vec<u8>, PostgresConnectError> {
588 let Some(hex) = text.strip_prefix("\\x") else {
589 return Ok(text.as_bytes().to_vec());
590 };
591 if hex.len() % 2 != 0 {
592 return Err(PostgresConnectError::Backend(format!(
593 "invalid bytea pgoutput text value {text:?}: odd hex length"
594 )));
595 }
596
597 hex.as_bytes()
598 .as_chunks::<2>()
599 .0
600 .iter()
601 .map(|chunk| {
602 let pair = std::str::from_utf8(chunk).map_err(|error| {
603 PostgresConnectError::Backend(format!(
604 "invalid bytea pgoutput text value {text:?}: {error}"
605 ))
606 })?;
607 u8::from_str_radix(pair, 16).map_err(|error| {
608 PostgresConnectError::Backend(format!(
609 "invalid bytea pgoutput text value {text:?}: {error}"
610 ))
611 })
612 })
613 .collect()
614}
615
616fn extract_key_columns(
617 schema: &TableSchema,
618 values: &[ColumnValue],
619) -> Result<Vec<ColumnValue>, PostgresConnectError> {
620 schema
621 .columns
622 .iter()
623 .filter(|column| column.key)
624 .map(|column| {
625 values
626 .iter()
627 .find(|value| value.name == column.name)
628 .cloned()
629 .ok_or_else(|| {
630 PostgresConnectError::Backend(format!(
631 "missing key column {:?} for {}.{}",
632 column.name, schema.schema, schema.table
633 ))
634 })
635 })
636 .collect()
637}
638
639fn has_any_key_column(schema: &TableSchema, values: &[ColumnValue]) -> bool {
640 schema
641 .columns
642 .iter()
643 .filter(|column| column.key)
644 .any(|column| values.iter().any(|value| value.name == column.name))
645}
646
647#[cfg(test)]
648mod tests {
649 use assert2::check;
650
651 use super::{
652 RelationCache, RelationEvent, RowEvent, RowEventKind, RowTupleKind, coerce_bytea,
653 coerce_value,
654 };
655 use crate::{
656 PgLsn, PostgresConnectError,
657 ids::{RelationId, TransactionId},
658 model::{
659 ColumnSchema, ColumnValue, EntityDifference, EntityKey, Operation, ScalarValue,
660 TableSchema,
661 },
662 };
663
664 fn orders_relation(type_name: &str) -> RelationEvent {
665 RelationEvent {
666 relation_id: RelationId(7),
667 schema: "public".to_owned(),
668 table: "orders".to_owned(),
669 columns: vec![
670 ColumnSchema {
671 name: "id".to_owned(),
672 type_name: "int8".to_owned(),
673 key: true,
674 },
675 ColumnSchema {
676 name: "status".to_owned(),
677 type_name: type_name.to_owned(),
678 key: false,
679 },
680 ],
681 }
682 }
683
684 fn orders_schema(type_name: &str) -> TableSchema {
685 let relation = orders_relation(type_name);
686 TableSchema {
687 schema: relation.schema,
688 table: relation.table,
689 columns: relation.columns,
690 }
691 }
692
693 fn id(value: i64) -> ColumnValue {
694 ColumnValue {
695 name: "id".to_owned(),
696 value: ScalarValue::Int(value),
697 }
698 }
699
700 fn status(value: &str) -> ColumnValue {
701 ColumnValue {
702 name: "status".to_owned(),
703 value: ScalarValue::Text(value.to_owned()),
704 }
705 }
706
707 fn tenant_id(value: i64) -> ColumnValue {
708 ColumnValue {
709 name: "tenant_id".to_owned(),
710 value: ScalarValue::Int(value),
711 }
712 }
713
714 #[test]
715 fn named_normalization_preserves_order_duplicates_and_whole_row_fallback() {
716 let mut cache = RelationCache::default();
717 cache.apply_relation(orders_relation("text"));
718 let relation = &cache.relations[&RelationId(7)];
719 let text = |name: &str, value: &str| ColumnValue {
720 name: name.into(),
721 value: ScalarValue::Text(value.into()),
722 };
723 let cases = [
724 (
725 vec![text("status", "paid"), text("id", "42")],
726 vec![status("paid"), id(42)],
727 ),
728 (vec![text("id", "7"), text("id", "8")], vec![id(7), id(8)]),
729 (
732 vec![text("status", "42"), text("missing", "paid")],
733 vec![id(42), status("paid")],
734 ),
735 (
736 vec![text("status", "42"), text("ID", "paid")],
737 vec![id(42), status("paid")],
738 ),
739 (
740 vec![
741 ColumnValue {
742 name: "status".into(),
743 value: ScalarValue::UnchangedToast,
744 },
745 ColumnValue {
746 name: "id".into(),
747 value: ScalarValue::Null,
748 },
749 ],
750 vec![
751 ColumnValue {
752 name: "status".into(),
753 value: ScalarValue::UnchangedToast,
754 },
755 ColumnValue {
756 name: "id".into(),
757 value: ScalarValue::Null,
758 },
759 ],
760 ),
761 ];
762 for (values, expected) in cases {
763 assert2::assert!(
764 super::normalize_values(relation, values, "row values").unwrap() == expected
765 );
766 }
767 let error = super::normalize_values(relation, vec![id(42)], "old row values").unwrap_err();
768 assert2::assert!(
769 error.to_string()
770 == "postgres backend error: column count mismatch for public.orders old row values: decoded 1 values but expected 2 columns"
771 );
772 let error = super::normalize_values(
773 relation,
774 vec![text("id", "invalid"), text("status", "paid")],
775 "row values",
776 )
777 .unwrap_err();
778 assert2::assert!(
779 error
780 .to_string()
781 .contains("invalid int8 pgoutput text value")
782 );
783 }
784
785 #[test]
786 fn relation_refresh_rebuilds_first_match_column_lookup() {
787 let mut cache = RelationCache::default();
788 let mut relation = orders_relation("text");
789 relation.columns[1].name = "id".into();
790 cache.apply_relation(relation.clone());
791 let values = vec![
792 ColumnValue {
793 name: "id".into(),
794 value: ScalarValue::Text("7".into()),
795 },
796 ColumnValue {
797 name: "id".into(),
798 value: ScalarValue::Text("8".into()),
799 },
800 ];
801 assert2::assert!(
802 super::normalize_values(
803 &cache.relations[&RelationId(7)],
804 values.clone(),
805 "row values"
806 )
807 .unwrap()
808 == vec![id(7), id(8)]
809 );
810 relation.columns.swap(0, 1);
811 cache.apply_relation(relation);
812 assert2::assert!(
813 super::normalize_values(
814 &cache.relations[&RelationId(7)],
815 values.clone(),
816 "row values"
817 )
818 .unwrap()
819 == values
820 );
821 cache.apply_relation(orders_relation("bool"));
822 assert2::assert!(
823 super::normalize_values(
824 &cache.relations[&RelationId(7)],
825 vec![
826 id(7),
827 ColumnValue {
828 name: "status".into(),
829 value: ScalarValue::Text("t".into())
830 }
831 ],
832 "row values",
833 )
834 .unwrap()
835 == vec![
836 id(7),
837 ColumnValue {
838 name: "status".into(),
839 value: ScalarValue::Bool(true)
840 }
841 ]
842 );
843 }
844
845 fn composite_key_orders_relation() -> RelationEvent {
846 RelationEvent {
847 relation_id: RelationId(7),
848 schema: "public".to_owned(),
849 table: "orders".to_owned(),
850 columns: vec![
851 ColumnSchema {
852 name: "tenant_id".to_owned(),
853 type_name: "int8".to_owned(),
854 key: true,
855 },
856 ColumnSchema {
857 name: "id".to_owned(),
858 type_name: "int8".to_owned(),
859 key: true,
860 },
861 ColumnSchema {
862 name: "status".to_owned(),
863 type_name: "text".to_owned(),
864 key: false,
865 },
866 ],
867 }
868 }
869
870 #[test]
871 fn insert_translates_to_entity_difference_with_key() {
872 let mut cache = RelationCache::default();
873 cache.apply_relation(orders_relation("text"));
874
875 let values = vec![id(42), status("paid")];
876 let difference = cache
877 .translate(RowEvent {
878 relation_id: RelationId(7),
879 lsn: PgLsn(0x16_b374_d848),
880 commit_lsn: None,
881 txid: Some(TransactionId(99)),
882 commit_timestamp_ms: Some(1_700_000_000_000),
883 kind: RowEventKind::Insert,
884 values: values.clone(),
885 })
886 .expect("relation should translate");
887
888 check!(
889 difference
890 == EntityDifference {
891 table: "public.orders".to_owned(),
892 key: EntityKey {
893 table: "public.orders".to_owned(),
894 columns: vec![id(42)],
895 },
896 op: Operation::Insert,
897 before: Vec::new(),
898 after: values,
899 lsn: PgLsn(0x16_b374_d848),
900 txid: Some(TransactionId(99)),
901 commit_timestamp_ms: Some(1_700_000_000_000),
902 schema: orders_schema("text"),
903 }
904 );
905 }
906
907 #[test]
908 fn delete_translates_to_before_only_difference() {
909 let mut cache = RelationCache::default();
910 cache.apply_relation(orders_relation("text"));
911
912 let values = vec![id(42), status("cancelled")];
913 let difference = cache
914 .translate(RowEvent {
915 relation_id: RelationId(7),
916 lsn: PgLsn(0x2a),
917 commit_lsn: None,
918 txid: None,
919 commit_timestamp_ms: None,
920 kind: RowEventKind::Delete {
921 tuple_kind: RowTupleKind::Full,
922 },
923 values: values.clone(),
924 })
925 .expect("relation should translate");
926
927 check!(
928 difference
929 == EntityDifference {
930 table: "public.orders".to_owned(),
931 key: EntityKey {
932 table: "public.orders".to_owned(),
933 columns: vec![id(42)],
934 },
935 op: Operation::Delete,
936 before: values,
937 after: Vec::new(),
938 lsn: PgLsn(0x2a),
939 txid: None,
940 commit_timestamp_ms: None,
941 schema: orders_schema("text"),
942 }
943 );
944 }
945
946 #[test]
947 fn relation_refresh_changes_table_schema() {
948 let mut cache = RelationCache::default();
949 cache.apply_relation(orders_relation("text"));
950 cache.apply_relation(orders_relation("varchar"));
951
952 let difference = cache
953 .translate(RowEvent {
954 relation_id: RelationId(7),
955 lsn: PgLsn(0x2b),
956 commit_lsn: None,
957 txid: None,
958 commit_timestamp_ms: None,
959 kind: RowEventKind::Insert,
960 values: vec![id(7), status("new")],
961 })
962 .expect("relation should translate");
963
964 check!(difference.schema.columns[1].type_name == "varchar");
965 }
966
967 #[test]
968 fn composite_key_output_follows_schema_order_even_if_row_values_are_out_of_order() {
969 let mut cache = RelationCache::default();
970 cache.apply_relation(composite_key_orders_relation());
971
972 let difference = cache
973 .translate(RowEvent {
974 relation_id: RelationId(7),
975 lsn: PgLsn(0x2c),
976 commit_lsn: None,
977 txid: None,
978 commit_timestamp_ms: None,
979 kind: RowEventKind::Insert,
980 values: vec![status("new"), id(7), tenant_id(3)],
981 })
982 .expect("relation should translate");
983
984 check!(difference.key.columns == vec![tenant_id(3), id(7)]);
985 }
986
987 #[test]
988 fn missing_key_column_is_an_error() {
989 let mut cache = RelationCache::default();
990 cache.apply_relation(orders_relation("text"));
991
992 let result = cache.translate(RowEvent {
993 relation_id: RelationId(7),
994 lsn: PgLsn(0x2d),
995 commit_lsn: None,
996 txid: None,
997 commit_timestamp_ms: None,
998 kind: RowEventKind::Insert,
999 values: vec![status("new"), status("paid")],
1000 });
1001
1002 let error = result.expect_err("missing key should fail");
1003 match error {
1004 PostgresConnectError::Backend(message) => {
1005 check!(message.contains("missing key column"));
1006 check!(message.contains("id"));
1007 }
1008 error => panic!("expected backend error, got {error:?}"),
1009 }
1010 }
1011
1012 #[test]
1013 fn non_key_update_translates_with_before_after_and_stable_key() {
1014 let mut cache = RelationCache::default();
1015 cache.apply_relation(orders_relation("text"));
1016
1017 let old = vec![id(42), status("new")];
1018 let new = vec![id(42), status("paid")];
1019 let difference = cache
1020 .translate(RowEvent {
1021 relation_id: RelationId(7),
1022 lsn: PgLsn(0x2e),
1023 commit_lsn: None,
1024 txid: Some(TransactionId(100)),
1025 commit_timestamp_ms: Some(1_700_000_000_001),
1026 kind: RowEventKind::Update {
1027 old: old.clone(),
1028 old_tuple_kind: RowTupleKind::Full,
1029 },
1030 values: new.clone(),
1031 })
1032 .expect("relation should translate");
1033
1034 check!(
1035 difference
1036 == EntityDifference {
1037 table: "public.orders".to_owned(),
1038 key: EntityKey {
1039 table: "public.orders".to_owned(),
1040 columns: vec![id(42)],
1041 },
1042 op: Operation::Update,
1043 before: old,
1044 after: new,
1045 lsn: PgLsn(0x2e),
1046 txid: Some(TransactionId(100)),
1047 commit_timestamp_ms: Some(1_700_000_000_001),
1048 schema: orders_schema("text"),
1049 }
1050 );
1051 }
1052
1053 #[test]
1054 fn key_changing_update_is_an_error() {
1055 let mut cache = RelationCache::default();
1056 cache.apply_relation(orders_relation("text"));
1057
1058 let result = cache.translate(RowEvent {
1059 relation_id: RelationId(7),
1060 lsn: PgLsn(0x2f),
1061 commit_lsn: None,
1062 txid: None,
1063 commit_timestamp_ms: None,
1064 kind: RowEventKind::Update {
1065 old: vec![id(41), status("new")],
1066 old_tuple_kind: RowTupleKind::Full,
1067 },
1068 values: vec![id(42), status("paid")],
1069 });
1070
1071 let error = result.expect_err("key-changing update should fail");
1072 match error {
1073 PostgresConnectError::Backend(message) => {
1074 check!(message == "key-changing updates are not supported");
1075 }
1076 error => panic!("expected backend error, got {error:?}"),
1077 }
1078 }
1079
1080 #[test]
1081 fn decoded_placeholder_values_bind_to_schema_names_before_key_extraction() {
1082 let mut cache = RelationCache::default();
1083 cache.apply_relation(orders_relation("text"));
1084
1085 let difference = cache
1086 .translate(RowEvent {
1087 relation_id: RelationId(7),
1088 lsn: PgLsn(0x30),
1089 commit_lsn: None,
1090 txid: None,
1091 commit_timestamp_ms: None,
1092 kind: RowEventKind::Insert,
1093 values: vec![
1094 ColumnValue {
1095 name: "col0".to_owned(),
1096 value: ScalarValue::Text("42".to_owned()),
1097 },
1098 ColumnValue {
1099 name: "col1".to_owned(),
1100 value: ScalarValue::Text("paid".to_owned()),
1101 },
1102 ],
1103 })
1104 .expect("decoded placeholders should bind to relation schema");
1105
1106 check!(
1107 (difference.key.columns, difference.after)
1108 == (vec![id(42)], vec![id(42), status("paid")],)
1109 );
1110 }
1111
1112 #[test]
1113 fn decoded_placeholder_count_mismatch_is_an_error() {
1114 let mut cache = RelationCache::default();
1115 cache.apply_relation(orders_relation("text"));
1116
1117 let error = cache
1118 .translate(RowEvent {
1119 relation_id: RelationId(7),
1120 lsn: PgLsn(0x31),
1121 commit_lsn: None,
1122 txid: None,
1123 commit_timestamp_ms: None,
1124 kind: RowEventKind::Insert,
1125 values: vec![ColumnValue {
1126 name: "col0".to_owned(),
1127 value: ScalarValue::Text("42".to_owned()),
1128 }],
1129 })
1130 .expect_err("decoded placeholder count should match relation schema");
1131
1132 match error {
1133 PostgresConnectError::Backend(message) => {
1134 check!(message.contains("column count mismatch"));
1135 check!(message.contains("public.orders"));
1136 }
1137 error => panic!("expected backend error, got {error:?}"),
1138 }
1139 }
1140
1141 #[test]
1142 fn named_partial_full_row_values_are_a_count_mismatch() {
1143 let mut cache = RelationCache::default();
1144 cache.apply_relation(orders_relation("text"));
1145
1146 let error = cache
1147 .translate(RowEvent {
1148 relation_id: RelationId(7),
1149 lsn: PgLsn(0x34),
1150 commit_lsn: None,
1151 txid: None,
1152 commit_timestamp_ms: None,
1153 kind: RowEventKind::Insert,
1154 values: vec![id(42)],
1155 })
1156 .expect_err("partial full row should fail count validation");
1157
1158 match error {
1159 PostgresConnectError::Backend(message) => {
1160 check!(message.contains("column count mismatch"));
1161 check!(message.contains("public.orders"));
1162 }
1163 error => panic!("expected backend error, got {error:?}"),
1164 }
1165 }
1166
1167 #[test]
1168 fn pgoutput_text_scalars_are_coerced_to_declared_column_types() {
1169 for (name, text, ty, expected) in [
1170 ("true-bool", "t", "bool", ScalarValue::Bool(true)),
1171 ("false-bool", "f", "bool", ScalarValue::Bool(false)),
1172 (
1173 "numeric",
1174 "12.50",
1175 "numeric",
1176 ScalarValue::Float("12.50".to_owned()),
1177 ),
1178 (
1179 "bytea",
1180 "\\xDEad",
1181 "bytea",
1182 ScalarValue::Bytes(vec![0xde, 0xad]),
1183 ),
1184 ] {
1185 check!(
1186 coerce_value(ScalarValue::Text(text.to_owned()), ty).expect("value coerces")
1187 == expected,
1188 "case {name}"
1189 );
1190 }
1191 }
1192
1193 #[test]
1194 fn invalid_bool_text_is_rejected() {
1195 let error = coerce_value(ScalarValue::Text("yes".to_owned()), "bool")
1196 .expect_err("invalid bool should fail");
1197
1198 match error {
1199 PostgresConnectError::Backend(message) => {
1200 check!(message.contains("invalid bool pgoutput text value"));
1201 check!(message.contains("yes"));
1202 }
1203 error => panic!("expected backend error, got {error:?}"),
1204 }
1205 }
1206
1207 #[test]
1208 fn bytea_text_supports_escape_and_raw_forms() {
1209 for (name, input, expected) in [
1210 ("hex", "\\x0001ff", vec![0, 1, 255]),
1211 ("raw", "raw", b"raw".to_vec()),
1212 ] {
1213 check!(
1214 coerce_bytea(input).expect("bytea coerces") == expected,
1215 "case {name}"
1216 );
1217 }
1218 }
1219
1220 #[test]
1221 fn bytea_hex_rejects_odd_length_and_invalid_pairs() {
1222 let odd = coerce_bytea("\\x0").expect_err("odd-length bytea should fail");
1223 let invalid = coerce_bytea("\\xzz").expect_err("invalid bytea should fail");
1224
1225 match odd {
1226 PostgresConnectError::Backend(message) => {
1227 check!(message.contains("odd hex length"));
1228 }
1229 error => panic!("expected backend error, got {error:?}"),
1230 }
1231 match invalid {
1232 PostgresConnectError::Backend(message) => {
1233 check!(message.contains("invalid bytea pgoutput text value"));
1234 check!(message.contains("zz"));
1235 }
1236 error => panic!("expected backend error, got {error:?}"),
1237 }
1238 }
1239
1240 #[test]
1241 fn update_without_old_tuple_does_not_trigger_key_change_check() {
1242 let mut cache = RelationCache::default();
1243 cache.apply_relation(orders_relation("text"));
1244
1245 let difference = cache
1246 .translate(RowEvent {
1247 relation_id: RelationId(7),
1248 lsn: PgLsn(0x35),
1249 commit_lsn: None,
1250 txid: None,
1251 commit_timestamp_ms: None,
1252 kind: RowEventKind::Update {
1253 old: Vec::new(),
1254 old_tuple_kind: RowTupleKind::Full,
1255 },
1256 values: vec![id(42), status("paid")],
1257 })
1258 .expect("missing old tuple should translate");
1259
1260 check!((difference.key.columns, difference.before) == (vec![id(42)], Vec::new()));
1261 }
1262
1263 #[test]
1264 fn key_column_presence_ignores_non_key_columns() {
1265 let schema = TableSchema {
1266 schema: "public".to_owned(),
1267 table: "orders".to_owned(),
1268 columns: orders_relation("text").columns,
1269 };
1270
1271 check!(super::has_any_key_column(&schema, &[id(42)]));
1272 check!(!super::has_any_key_column(&schema, &[status("paid")]));
1273 check!(!super::has_any_key_column(&schema, &[]));
1274 }
1275
1276 #[test]
1277 fn type_oids_map_to_supported_scalar_names() {
1278 for (oid, name) in [
1279 (16, "bool"),
1280 (17, "bytea"),
1281 (20, "int8"),
1282 (21, "int2"),
1283 (23, "int4"),
1284 (25, "text"),
1285 (700, "float4"),
1286 (701, "float8"),
1287 (1043, "varchar"),
1288 (1700, "numeric"),
1289 (999_999, "unknown"),
1290 ] {
1291 check!(super::type_name(oid) == name);
1292 }
1293 }
1294}
1295
1296#[cfg(test)]
1297mod decode_tests {
1298 use assert2::check;
1299
1300 use super::{
1301 DecodedMessage, RelationEvent, RowEvent, RowEventKind, RowTupleKind,
1302 decode_pgoutput_message,
1303 };
1304 use crate::{
1305 PgLsn, PostgresConnectError,
1306 ids::{CommitLsn, EndLsn, RelationId, TransactionId},
1307 model::{
1308 ColumnSchema, ColumnValue, EntityDifference, EntityKey, Operation, ScalarValue,
1309 TableSchema,
1310 },
1311 };
1312
1313 fn put_i16(bytes: &mut Vec<u8>, value: i16) {
1314 bytes.extend_from_slice(&value.to_be_bytes());
1315 }
1316
1317 fn put_i32(bytes: &mut Vec<u8>, value: i32) {
1318 bytes.extend_from_slice(&value.to_be_bytes());
1319 }
1320
1321 fn put_u32(bytes: &mut Vec<u8>, value: u32) {
1322 bytes.extend_from_slice(&value.to_be_bytes());
1323 }
1324
1325 fn put_i64(bytes: &mut Vec<u8>, value: i64) {
1326 bytes.extend_from_slice(&value.to_be_bytes());
1327 }
1328
1329 fn put_cstr(bytes: &mut Vec<u8>, value: &str) {
1330 bytes.extend_from_slice(value.as_bytes());
1331 bytes.push(0);
1332 }
1333
1334 fn put_text_value(bytes: &mut Vec<u8>, value: &str) {
1335 bytes.push(b't');
1336 let length = i32::try_from(value.len()).expect("test value length should fit i32");
1337 put_i32(bytes, length);
1338 bytes.extend_from_slice(value.as_bytes());
1339 }
1340
1341 fn orders_relation_message() -> super::RelationEvent {
1342 super::RelationEvent {
1343 relation_id: RelationId(7),
1344 schema: "public".to_owned(),
1345 table: "orders".to_owned(),
1346 columns: vec![
1347 ColumnSchema {
1348 name: "id".to_owned(),
1349 type_name: "int8".to_owned(),
1350 key: true,
1351 },
1352 ColumnSchema {
1353 name: "status".to_owned(),
1354 type_name: "text".to_owned(),
1355 key: false,
1356 },
1357 ],
1358 }
1359 }
1360
1361 fn wide_relation_message(columns: i16) -> Vec<u8> {
1362 let mut bytes = vec![b'R'];
1363 put_i32(&mut bytes, 7);
1364 put_cstr(&mut bytes, "public");
1365 put_cstr(&mut bytes, "wide");
1366 bytes.push(b'd');
1367 put_i16(&mut bytes, columns);
1368 for index in 0..columns {
1369 bytes.push(u8::from(index == 0));
1370 put_cstr(&mut bytes, &format!("col{index}"));
1371 put_i32(&mut bytes, 20);
1372 put_i32(&mut bytes, -1);
1373 }
1374 bytes
1375 }
1376
1377 fn put_wide_tuple(bytes: &mut Vec<u8>, columns: i16) {
1378 put_i16(bytes, columns);
1379 for index in 0..columns {
1380 if index == columns - 2 {
1381 bytes.push(b'n');
1382 } else if index == columns - 1 {
1383 bytes.push(b'u');
1384 } else {
1385 put_text_value(bytes, &index.to_string());
1386 }
1387 }
1388 }
1389
1390 #[test]
1391 fn matching_col_names_decode_and_translate_wide_insert_update_and_delete() {
1392 for columns in [100, 500, 1_000, 1_600] {
1393 let DecodedMessage::Relation(relation) =
1394 decode_pgoutput_message(&wide_relation_message(columns), PgLsn(0), None).unwrap()
1395 else {
1396 panic!("expected relation")
1397 };
1398 let schema = TableSchema {
1399 schema: relation.schema.clone(),
1400 table: relation.table.clone(),
1401 columns: relation.columns.clone(),
1402 };
1403 let mut cache = super::RelationCache::default();
1404 cache.apply_relation(relation);
1405 let expected_values: Vec<_> = (0..columns)
1406 .map(|index| ColumnValue {
1407 name: format!("col{index}"),
1408 value: if index == columns - 2 {
1409 ScalarValue::Null
1410 } else if index == columns - 1 {
1411 ScalarValue::UnchangedToast
1412 } else {
1413 ScalarValue::Int(i64::from(index))
1414 },
1415 })
1416 .collect();
1417 for (tag, op) in [
1418 (b'I', Operation::Insert),
1419 (b'U', Operation::Update),
1420 (b'D', Operation::Delete),
1421 ] {
1422 let mut bytes = vec![tag];
1423 put_i32(&mut bytes, 7);
1424 if tag == b'U' {
1425 bytes.push(b'O');
1426 put_wide_tuple(&mut bytes, columns);
1427 }
1428 bytes.push(if tag == b'D' { b'O' } else { b'N' });
1429 put_wide_tuple(&mut bytes, columns);
1430 let DecodedMessage::Row(row) =
1431 decode_pgoutput_message(&bytes, PgLsn(50), Some(TransactionId(101))).unwrap()
1432 else {
1433 panic!("expected row")
1434 };
1435 let expected = EntityDifference {
1436 table: "public.wide".into(),
1437 key: EntityKey {
1438 table: "public.wide".into(),
1439 columns: vec![expected_values[0].clone()],
1440 },
1441 op,
1442 before: if op == Operation::Insert {
1443 Vec::new()
1444 } else {
1445 expected_values.clone()
1446 },
1447 after: if op == Operation::Delete {
1448 Vec::new()
1449 } else {
1450 expected_values.clone()
1451 },
1452 lsn: PgLsn(50),
1453 txid: Some(TransactionId(101)),
1454 commit_timestamp_ms: None,
1455 schema: schema.clone(),
1456 };
1457 assert2::assert!(cache.translate(row).unwrap() == expected);
1458 }
1459 }
1460 }
1461
1462 #[test]
1463 fn decodes_relation_message() {
1464 let mut bytes = vec![b'R'];
1465 put_i32(&mut bytes, 7);
1466 put_cstr(&mut bytes, "public");
1467 put_cstr(&mut bytes, "orders");
1468 bytes.push(b'd');
1469 put_i16(&mut bytes, 2);
1470 bytes.push(1);
1471 put_cstr(&mut bytes, "id");
1472 put_i32(&mut bytes, 20);
1473 put_i32(&mut bytes, -1);
1474 bytes.push(0);
1475 put_cstr(&mut bytes, "status");
1476 put_i32(&mut bytes, 25);
1477 put_i32(&mut bytes, -1);
1478
1479 let decoded = decode_pgoutput_message(&bytes, PgLsn(0x2a), Some(TransactionId(99)))
1480 .expect("relation message should decode");
1481
1482 check!(
1483 decoded
1484 == DecodedMessage::Relation(RelationEvent {
1485 relation_id: RelationId(7),
1486 schema: "public".to_owned(),
1487 table: "orders".to_owned(),
1488 columns: vec![
1489 ColumnSchema {
1490 name: "id".to_owned(),
1491 type_name: "int8".to_owned(),
1492 key: true,
1493 },
1494 ColumnSchema {
1495 name: "status".to_owned(),
1496 type_name: "text".to_owned(),
1497 key: false,
1498 },
1499 ],
1500 })
1501 );
1502 }
1503
1504 #[test]
1505 fn decodes_insert_message_to_row_event() {
1506 let mut bytes = vec![b'I'];
1507 put_i32(&mut bytes, 7);
1508 bytes.push(b'N');
1509 put_i16(&mut bytes, 2);
1510 put_text_value(&mut bytes, "42");
1511 put_text_value(&mut bytes, "paid");
1512
1513 let decoded = decode_pgoutput_message(&bytes, PgLsn(0x2a), Some(TransactionId(99)))
1514 .expect("insert message should decode");
1515
1516 check!(
1517 decoded
1518 == DecodedMessage::Row(RowEvent {
1519 relation_id: RelationId(7),
1520 lsn: PgLsn(0x2a),
1521 commit_lsn: None,
1522 txid: Some(TransactionId(99)),
1523 commit_timestamp_ms: None,
1524 kind: RowEventKind::Insert,
1525 values: vec![
1526 ColumnValue {
1527 name: "col0".to_owned(),
1528 value: ScalarValue::Text("42".to_owned()),
1529 },
1530 ColumnValue {
1531 name: "col1".to_owned(),
1532 value: ScalarValue::Text("paid".to_owned()),
1533 },
1534 ],
1535 })
1536 );
1537 }
1538
1539 #[test]
1540 fn decoded_relation_and_insert_translate_with_schema_column_names() {
1541 let mut relation_bytes = vec![b'R'];
1542 put_i32(&mut relation_bytes, 7);
1543 put_cstr(&mut relation_bytes, "public");
1544 put_cstr(&mut relation_bytes, "orders");
1545 relation_bytes.push(b'd');
1546 put_i16(&mut relation_bytes, 2);
1547 relation_bytes.push(1);
1548 put_cstr(&mut relation_bytes, "id");
1549 put_i32(&mut relation_bytes, 20);
1550 put_i32(&mut relation_bytes, -1);
1551 relation_bytes.push(0);
1552 put_cstr(&mut relation_bytes, "status");
1553 put_i32(&mut relation_bytes, 25);
1554 put_i32(&mut relation_bytes, -1);
1555
1556 let mut cache = super::RelationCache::default();
1557 let DecodedMessage::Relation(relation) =
1558 decode_pgoutput_message(&relation_bytes, PgLsn(0), None)
1559 .expect("relation should decode")
1560 else {
1561 panic!("expected relation");
1562 };
1563 cache.apply_relation(relation);
1564
1565 let mut insert_bytes = vec![b'I'];
1566 put_i32(&mut insert_bytes, 7);
1567 insert_bytes.push(b'N');
1568 put_i16(&mut insert_bytes, 2);
1569 put_text_value(&mut insert_bytes, "42");
1570 put_text_value(&mut insert_bytes, "paid");
1571
1572 let DecodedMessage::Row(row) =
1573 decode_pgoutput_message(&insert_bytes, PgLsn(0x32), Some(TransactionId(101)))
1574 .expect("insert should decode")
1575 else {
1576 panic!("expected row");
1577 };
1578 let difference = cache
1579 .translate(row)
1580 .expect("decoded insert should translate");
1581
1582 check!(
1583 (difference.key.columns, difference.after)
1584 == (
1585 vec![ColumnValue {
1586 name: "id".to_owned(),
1587 value: ScalarValue::Int(42),
1588 }],
1589 vec![
1590 ColumnValue {
1591 name: "id".to_owned(),
1592 value: ScalarValue::Int(42),
1593 },
1594 ColumnValue {
1595 name: "status".to_owned(),
1596 value: ScalarValue::Text("paid".to_owned()),
1597 },
1598 ],
1599 )
1600 );
1601 }
1602
1603 #[test]
1604 fn decoded_int8_text_value_translates_to_int_scalar_for_key() {
1605 let mut cache = super::RelationCache::default();
1606 cache.apply_relation(orders_relation_message());
1607
1608 let mut insert_bytes = vec![b'I'];
1609 put_i32(&mut insert_bytes, 7);
1610 insert_bytes.push(b'N');
1611 put_i16(&mut insert_bytes, 2);
1612 put_text_value(&mut insert_bytes, "42");
1613 put_text_value(&mut insert_bytes, "paid");
1614
1615 let DecodedMessage::Row(row) =
1616 decode_pgoutput_message(&insert_bytes, PgLsn(0x32), Some(TransactionId(101)))
1617 .expect("insert should decode")
1618 else {
1619 panic!("expected row");
1620 };
1621 let difference = cache
1622 .translate(row)
1623 .expect("decoded insert should translate");
1624
1625 check!(
1626 difference.key.columns
1627 == vec![ColumnValue {
1628 name: "id".to_owned(),
1629 value: ScalarValue::Int(42),
1630 }]
1631 );
1632 }
1633
1634 #[test]
1635 fn decoded_delete_key_tuple_translates_with_key_column_name() {
1636 let mut cache = super::RelationCache::default();
1637 cache.apply_relation(orders_relation_message());
1638
1639 let mut delete_bytes = vec![b'D'];
1640 put_i32(&mut delete_bytes, 7);
1641 delete_bytes.push(b'K');
1642 put_i16(&mut delete_bytes, 2);
1643 put_text_value(&mut delete_bytes, "42");
1644 delete_bytes.push(b'n');
1645
1646 let DecodedMessage::Row(row) =
1647 decode_pgoutput_message(&delete_bytes, PgLsn(0x35), Some(TransactionId(102)))
1648 .expect("delete should decode")
1649 else {
1650 panic!("expected row");
1651 };
1652 let difference = cache
1653 .translate(row)
1654 .expect("decoded delete should translate");
1655
1656 let expected = vec![ColumnValue {
1657 name: "id".to_owned(),
1658 value: ScalarValue::Int(42),
1659 }];
1660 check!(
1661 (difference.key.columns, difference.before)
1662 == (
1663 expected,
1664 vec![
1665 ColumnValue {
1666 name: "id".to_owned(),
1667 value: ScalarValue::Int(42),
1668 },
1669 ColumnValue {
1670 name: "status".to_owned(),
1671 value: ScalarValue::Null,
1672 },
1673 ],
1674 )
1675 );
1676 }
1677
1678 #[test]
1679 fn decoded_update_key_old_tuple_translates_for_non_key_update() {
1680 let mut cache = super::RelationCache::default();
1681 cache.apply_relation(orders_relation_message());
1682
1683 let mut update_bytes = vec![b'U'];
1684 put_i32(&mut update_bytes, 7);
1685 update_bytes.push(b'K');
1686 put_i16(&mut update_bytes, 2);
1687 put_text_value(&mut update_bytes, "42");
1688 update_bytes.push(b'n');
1689 update_bytes.push(b'N');
1690 put_i16(&mut update_bytes, 2);
1691 put_text_value(&mut update_bytes, "42");
1692 put_text_value(&mut update_bytes, "paid");
1693
1694 let DecodedMessage::Row(row) =
1695 decode_pgoutput_message(&update_bytes, PgLsn(0x36), Some(TransactionId(103)))
1696 .expect("update should decode")
1697 else {
1698 panic!("expected row");
1699 };
1700 let difference = cache
1701 .translate(row)
1702 .expect("decoded update should translate");
1703
1704 check!(
1705 (difference.before, difference.after)
1706 == (
1707 vec![
1708 ColumnValue {
1709 name: "id".to_owned(),
1710 value: ScalarValue::Int(42),
1711 },
1712 ColumnValue {
1713 name: "status".to_owned(),
1714 value: ScalarValue::Null,
1715 }
1716 ],
1717 vec![
1718 ColumnValue {
1719 name: "id".to_owned(),
1720 value: ScalarValue::Int(42),
1721 },
1722 ColumnValue {
1723 name: "status".to_owned(),
1724 value: ScalarValue::Text("paid".to_owned()),
1725 },
1726 ],
1727 )
1728 );
1729 }
1730
1731 #[test]
1732 fn decodes_delete_message_to_row_event() {
1733 let mut bytes = vec![b'D'];
1734 put_i32(&mut bytes, 7);
1735 bytes.push(b'K');
1736 put_i16(&mut bytes, 2);
1737 put_text_value(&mut bytes, "42");
1738 bytes.push(b'n');
1739
1740 let decoded = decode_pgoutput_message(&bytes, PgLsn(0x2b), None)
1741 .expect("delete message should decode");
1742
1743 check!(
1744 decoded
1745 == DecodedMessage::Row(RowEvent {
1746 relation_id: RelationId(7),
1747 lsn: PgLsn(0x2b),
1748 commit_lsn: None,
1749 txid: None,
1750 commit_timestamp_ms: None,
1751 kind: RowEventKind::Delete {
1752 tuple_kind: RowTupleKind::Key,
1753 },
1754 values: vec![
1755 ColumnValue {
1756 name: "col0".to_owned(),
1757 value: ScalarValue::Text("42".to_owned()),
1758 },
1759 ColumnValue {
1760 name: "col1".to_owned(),
1761 value: ScalarValue::Null,
1762 },
1763 ],
1764 })
1765 );
1766 }
1767
1768 #[test]
1769 fn decodes_update_message_to_row_event() {
1770 let mut bytes = vec![b'U'];
1771 put_i32(&mut bytes, 7);
1772 bytes.push(b'K');
1773 put_i16(&mut bytes, 2);
1774 put_text_value(&mut bytes, "41");
1775 bytes.push(b'n');
1776 bytes.push(b'N');
1777 put_i16(&mut bytes, 2);
1778 put_text_value(&mut bytes, "42");
1779 put_text_value(&mut bytes, "paid");
1780
1781 let decoded = decode_pgoutput_message(&bytes, PgLsn(0x2c), Some(TransactionId(100)))
1782 .expect("update message should decode");
1783
1784 check!(
1785 decoded
1786 == DecodedMessage::Row(RowEvent {
1787 relation_id: RelationId(7),
1788 lsn: PgLsn(0x2c),
1789 commit_lsn: None,
1790 txid: Some(TransactionId(100)),
1791 commit_timestamp_ms: None,
1792 kind: RowEventKind::Update {
1793 old: vec![
1794 ColumnValue {
1795 name: "col0".to_owned(),
1796 value: ScalarValue::Text("41".to_owned()),
1797 },
1798 ColumnValue {
1799 name: "col1".to_owned(),
1800 value: ScalarValue::Null,
1801 },
1802 ],
1803 old_tuple_kind: RowTupleKind::Key,
1804 },
1805 values: vec![
1806 ColumnValue {
1807 name: "col0".to_owned(),
1808 value: ScalarValue::Text("42".to_owned()),
1809 },
1810 ColumnValue {
1811 name: "col1".to_owned(),
1812 value: ScalarValue::Text("paid".to_owned()),
1813 },
1814 ],
1815 })
1816 );
1817 }
1818
1819 #[test]
1820 fn decodes_unchanged_toast_as_distinct_scalar_value() {
1821 let mut bytes = vec![b'U'];
1822 put_i32(&mut bytes, 7);
1823 bytes.push(b'N');
1824 put_i16(&mut bytes, 2);
1825 put_text_value(&mut bytes, "42");
1826 bytes.push(b'u');
1827
1828 let decoded = decode_pgoutput_message(&bytes, PgLsn(0x33), Some(TransactionId(100)))
1829 .expect("update message should decode");
1830
1831 let DecodedMessage::Row(row) = decoded else {
1832 panic!("expected row message");
1833 };
1834 check!(row.values[1].value == ScalarValue::UnchangedToast);
1835 }
1836
1837 #[test]
1838 fn malformed_message_returns_backend_error() {
1839 let error = decode_pgoutput_message(&[b'R', 0, 0], PgLsn(0), None)
1840 .expect_err("truncated message should fail");
1841
1842 match error {
1843 PostgresConnectError::Backend(message) => {
1844 check!(message.contains("truncated"));
1845 }
1846 error => panic!("expected backend error, got {error:?}"),
1847 }
1848 }
1849
1850 #[test]
1851 fn trailing_message_bytes_are_rejected() {
1852 let mut bytes = vec![b'I'];
1853 put_i32(&mut bytes, 7);
1854 bytes.push(b'N');
1855 put_i16(&mut bytes, 2);
1856 put_text_value(&mut bytes, "42");
1857 put_text_value(&mut bytes, "paid");
1858 bytes.push(0xff);
1859
1860 let error = decode_pgoutput_message(&bytes, PgLsn(0), None)
1861 .expect_err("trailing bytes should fail");
1862
1863 match error {
1864 PostgresConnectError::Backend(message) => {
1865 check!(message.contains("trailing bytes"));
1866 }
1867 error => panic!("expected backend error, got {error:?}"),
1868 }
1869 }
1870
1871 #[test]
1872 fn decodes_begin_and_commit_messages() {
1873 let mut begin = vec![b'B'];
1874 put_i64(&mut begin, 0x0102_0304);
1875 put_i64(&mut begin, 1_700_000_000);
1876 put_i32(&mut begin, 123);
1877
1878 let decoded = decode_pgoutput_message(&begin, PgLsn(0), None).expect("begin should decode");
1879 check!(
1880 decoded
1881 == DecodedMessage::Begin {
1882 final_lsn: PgLsn(0x0102_0304),
1883 xid: TransactionId(123),
1884 }
1885 );
1886
1887 let mut commit = vec![b'C', 0];
1888 put_i64(&mut commit, 0x0102_0305);
1889 put_i64(&mut commit, 0x0102_0306);
1890 put_i64(&mut commit, 1_000);
1891
1892 let decoded =
1893 decode_pgoutput_message(&commit, PgLsn(0), None).expect("commit should decode");
1894 check!(
1895 decoded
1896 == DecodedMessage::Commit {
1897 commit_lsn: CommitLsn(PgLsn(0x0102_0305)),
1898 end_lsn: EndLsn(PgLsn(0x0102_0306)),
1899 commit_timestamp_ms: 946_684_800_001,
1900 }
1901 );
1902 }
1903
1904 #[test]
1905 fn decodes_begin_xid_as_unsigned_u32() {
1906 let mut begin = vec![b'B'];
1907 put_i64(&mut begin, 0x0102_0304);
1908 put_i64(&mut begin, 1_700_000_000);
1909 put_u32(&mut begin, 0x8000_0000);
1910
1911 let decoded = decode_pgoutput_message(&begin, PgLsn(0), None).expect("begin should decode");
1912 check!(
1913 decoded
1914 == DecodedMessage::Begin {
1915 final_lsn: PgLsn(0x0102_0304),
1916 xid: TransactionId(2_147_483_648),
1917 }
1918 );
1919 }
1920}