1use anyhow::{anyhow, Result};
16use byteorder::{BigEndian, ReadBytesExt};
17use chrono::{DateTime, NaiveDateTime, Utc};
18use log::{debug, warn};
19use postgres_types::Oid;
20use rust_decimal::Decimal;
21use serde_json::Value as JsonValue;
22use std::collections::HashMap;
23use std::io::{Cursor, Read};
24use std::sync::Arc;
25use uuid::Uuid;
26
27use super::types::{
28 ColumnInfo, PostgresValue, RelationInfo, ReplicaIdentity, TransactionInfo, WalMessage,
29};
30
31#[allow(dead_code)]
32const PGOUTPUT_VERSION: u32 = 1;
33
34pub struct PgOutputDecoder {
35 relations: HashMap<u32, RelationInfo>,
36 current_transaction: Option<TransactionInfo>,
37}
38
39impl Default for PgOutputDecoder {
40 fn default() -> Self {
41 Self::new()
42 }
43}
44
45impl PgOutputDecoder {
46 pub fn new() -> Self {
47 Self {
48 relations: HashMap::new(),
49 current_transaction: None,
50 }
51 }
52
53 pub fn decode_message(&mut self, data: &[u8]) -> Result<Option<WalMessage>> {
54 if data.is_empty() {
55 return Ok(None);
56 }
57
58 let msg_type = data[0];
59 let payload = &data[1..];
60
61 match msg_type {
62 b'B' => self.decode_begin(payload),
63 b'C' => self.decode_commit(payload),
64 b'O' => self.decode_origin(payload),
65 b'R' => self.decode_relation(payload),
66 b'Y' => self.decode_type(payload),
67 b'I' => self.decode_insert(payload),
68 b'U' => self.decode_update(payload),
69 b'D' => self.decode_delete(payload),
70 b'T' => self.decode_truncate(payload),
71 b'M' => self.decode_message_logical(payload),
72 _ => {
73 warn!("Unknown pgoutput message type: 0x{msg_type:02x}");
74 Ok(None)
75 }
76 }
77 }
78
79 fn decode_begin(&mut self, data: &[u8]) -> Result<Option<WalMessage>> {
80 let mut cursor = Cursor::new(data);
81
82 let final_lsn = cursor.read_u64::<BigEndian>()?;
83 let commit_timestamp = cursor.read_i64::<BigEndian>()?;
84 let xid = cursor.read_u32::<BigEndian>()?;
85
86 let timestamp = postgres_epoch_to_datetime(commit_timestamp)?;
88
89 let transaction = TransactionInfo {
90 xid,
91 commit_lsn: final_lsn,
92 commit_timestamp: timestamp,
93 };
94
95 self.current_transaction = Some(transaction.clone());
96
97 debug!("Begin transaction: xid={xid}, lsn={final_lsn:x}");
98 Ok(Some(WalMessage::Begin(transaction)))
99 }
100
101 fn decode_commit(&mut self, data: &[u8]) -> Result<Option<WalMessage>> {
102 let mut cursor = Cursor::new(data);
103
104 let _flags = cursor.read_u8()?;
105 let commit_lsn = cursor.read_u64::<BigEndian>()?;
106 let _end_lsn = cursor.read_u64::<BigEndian>()?;
107 let commit_timestamp = cursor.read_i64::<BigEndian>()?;
108
109 let timestamp = postgres_epoch_to_datetime(commit_timestamp)?;
110
111 let transaction = TransactionInfo {
112 xid: self
113 .current_transaction
114 .as_ref()
115 .map(|t| t.xid)
116 .unwrap_or(0),
117 commit_lsn,
118 commit_timestamp: timestamp,
119 };
120
121 self.current_transaction = None;
122
123 debug!("Commit transaction: lsn={commit_lsn:x}");
124 Ok(Some(WalMessage::Commit(transaction)))
125 }
126
127 fn decode_origin(&mut self, _data: &[u8]) -> Result<Option<WalMessage>> {
128 Ok(None)
130 }
131
132 fn decode_relation(&mut self, data: &[u8]) -> Result<Option<WalMessage>> {
133 let mut cursor = Cursor::new(data);
134
135 let relation_id = cursor.read_u32::<BigEndian>()?;
136 let namespace = read_cstring(&mut cursor)?;
137 let name = read_cstring(&mut cursor)?;
138 let replica_identity = match cursor.read_u8()? {
139 b'd' => ReplicaIdentity::Default,
140 b'n' => ReplicaIdentity::Nothing,
141 b'f' => ReplicaIdentity::Full,
142 b'i' => ReplicaIdentity::Index,
143 other => {
144 warn!("Unknown replica identity: {}", other as char);
145 ReplicaIdentity::Default
146 }
147 };
148
149 let column_count = cursor.read_u16::<BigEndian>()?;
150 let mut columns = Vec::with_capacity(column_count as usize);
151
152 for _ in 0..column_count {
153 let flags = cursor.read_u8()?;
154 let is_key = (flags & 1) != 0;
155 let column_name = read_cstring(&mut cursor)?;
156 let type_oid = cursor.read_u32::<BigEndian>()?;
157 let type_modifier = cursor.read_i32::<BigEndian>()?;
158
159 columns.push(ColumnInfo {
160 name: column_name,
161 type_oid: Oid::from(type_oid),
162 type_modifier,
163 is_key,
164 });
165 }
166
167 let relation = RelationInfo {
168 id: relation_id,
169 namespace,
170 name: name.clone(),
171 replica_identity,
172 columns,
173 };
174
175 debug!(
176 "Relation: id={}, namespace={}, name={}, columns={}",
177 relation_id,
178 relation.namespace,
179 name,
180 relation.columns.len()
181 );
182
183 self.relations.insert(relation_id, relation.clone());
184 Ok(Some(WalMessage::Relation(relation)))
185 }
186
187 fn decode_type(&mut self, _data: &[u8]) -> Result<Option<WalMessage>> {
188 Ok(None)
190 }
191
192 fn decode_insert(&mut self, data: &[u8]) -> Result<Option<WalMessage>> {
193 debug!("Decoding insert message, data length: {}", data.len());
194 let mut cursor = Cursor::new(data);
195
196 let relation_id = cursor.read_u32::<BigEndian>()?;
197 let tuple_type = cursor.read_u8()?;
198
199 debug!(
200 "Insert: relation_id={}, tuple_type={}",
201 relation_id, tuple_type as char
202 );
203
204 if tuple_type != b'N' {
205 return Err(anyhow!(
206 "Expected 'N' tuple type for insert, got: {tuple_type}"
207 ));
208 }
209
210 let relation = self
211 .relations
212 .get(&relation_id)
213 .ok_or_else(|| anyhow!("Unknown relation ID: {relation_id}"))?;
214
215 debug!(
216 "Insert for table: {}, expected columns: {}",
217 relation.name,
218 relation.columns.len()
219 );
220 let tuple = self.decode_tuple_data(&mut cursor, &relation.columns)?;
221
222 debug!(
223 "Insert: relation={}, columns={}",
224 relation.name,
225 tuple.len()
226 );
227 Ok(Some(WalMessage::Insert { relation_id, tuple }))
228 }
229
230 fn decode_update(&mut self, data: &[u8]) -> Result<Option<WalMessage>> {
231 let mut cursor = Cursor::new(data);
232
233 let relation_id = cursor.read_u32::<BigEndian>()?;
234 let tuple_type = cursor.read_u8()?;
235
236 let relation = self
237 .relations
238 .get(&relation_id)
239 .ok_or_else(|| anyhow!("Unknown relation ID: {relation_id}"))?;
240
241 let mut old_tuple = None;
242 let new_tuple;
243
244 match tuple_type {
245 b'K' | b'O' => {
246 old_tuple = Some(self.decode_tuple_data(&mut cursor, &relation.columns)?);
248 let next_type = cursor.read_u8()?;
249 if next_type != b'N' {
250 return Err(anyhow!("Expected 'N' after old tuple, got: {next_type}"));
251 }
252 new_tuple = self.decode_tuple_data(&mut cursor, &relation.columns)?;
253 }
254 b'N' => {
255 new_tuple = self.decode_tuple_data(&mut cursor, &relation.columns)?;
257 }
258 _ => {
259 return Err(anyhow!("Unknown tuple type for update: {tuple_type}"));
260 }
261 }
262
263 debug!(
264 "Update: relation={}, has_old={}",
265 relation.name,
266 old_tuple.is_some()
267 );
268 Ok(Some(WalMessage::Update {
269 relation_id,
270 old_tuple,
271 new_tuple,
272 }))
273 }
274
275 fn decode_delete(&mut self, data: &[u8]) -> Result<Option<WalMessage>> {
276 let mut cursor = Cursor::new(data);
277
278 let relation_id = cursor.read_u32::<BigEndian>()?;
279 let tuple_type = cursor.read_u8()?;
280
281 if tuple_type != b'K' && tuple_type != b'O' {
282 return Err(anyhow!(
283 "Expected 'K' or 'O' tuple type for delete, got: {tuple_type}"
284 ));
285 }
286
287 let relation = self
288 .relations
289 .get(&relation_id)
290 .ok_or_else(|| anyhow!("Unknown relation ID: {relation_id}"))?;
291
292 let old_tuple = self.decode_tuple_data(&mut cursor, &relation.columns)?;
293
294 debug!("Delete: relation={}", relation.name);
295 Ok(Some(WalMessage::Delete {
296 relation_id,
297 old_tuple,
298 }))
299 }
300
301 fn decode_truncate(&mut self, data: &[u8]) -> Result<Option<WalMessage>> {
302 let mut cursor = Cursor::new(data);
303
304 let relation_count = cursor.read_u32::<BigEndian>()?;
305 let _options = cursor.read_u8()?;
306
307 let mut relation_ids = Vec::with_capacity(relation_count as usize);
308 for _ in 0..relation_count {
309 relation_ids.push(cursor.read_u32::<BigEndian>()?);
310 }
311
312 debug!("Truncate: {relation_count} relations");
313 Ok(Some(WalMessage::Truncate { relation_ids }))
314 }
315
316 fn decode_message_logical(&mut self, _data: &[u8]) -> Result<Option<WalMessage>> {
317 Ok(None)
319 }
320
321 fn decode_tuple_data(
322 &self,
323 cursor: &mut Cursor<&[u8]>,
324 columns: &[ColumnInfo],
325 ) -> Result<Vec<PostgresValue>> {
326 let start_pos = cursor.position();
327 let total_len = cursor.get_ref().len();
328 debug!("decode_tuple_data: start position={start_pos}, total buffer length={total_len}");
329
330 let column_count = cursor.read_u16::<BigEndian>()? as usize;
331 debug!("Decoding {column_count} columns");
332
333 if column_count != columns.len() {
334 warn!(
335 "Column count mismatch: expected {}, got {}",
336 columns.len(),
337 column_count
338 );
339 }
340
341 let mut values = Vec::with_capacity(column_count);
342
343 for i in 0..column_count {
344 let column = columns
345 .get(i)
346 .ok_or_else(|| anyhow!("Column index out of bounds: {i}"))?;
347
348 let tuple_type = cursor.read_u8()?;
349 debug!(
350 "Column {}: type={} ({}), oid={}",
351 i, tuple_type as char, tuple_type, column.type_oid
352 );
353
354 let value = match tuple_type {
355 b'n' => PostgresValue::Null,
356 b'u' => PostgresValue::Null, b't' => {
358 let length = cursor.read_u32::<BigEndian>()? as usize;
359 let pos = cursor.position() as usize;
361 let available = cursor.get_ref().len() - pos;
362 debug!(
363 "Column {i} text value: length={length}, pos={pos}, available={available}"
364 );
365 if available < length {
366 return Err(anyhow!("Not enough data for column {} ({}): need {} bytes, have {} bytes at position {}",
367 i, column.name, length, available, pos));
368 }
369 let mut data = vec![0u8; length];
370 cursor.read_exact(&mut data).map_err(|e| {
371 anyhow!(
372 "Failed to read {} bytes for column {} ({}): {}",
373 length,
374 i,
375 column.name,
376 e
377 )
378 })?;
379 debug!(
380 "Successfully read {} bytes for column {}, decoding type OID {}",
381 length, i, column.type_oid
382 );
383 self.decode_column_value(&data, column.type_oid)?
384 }
385 _ => {
386 return Err(anyhow!(
387 "Unknown tuple data type: {} ({})",
388 tuple_type as char,
389 tuple_type
390 ));
391 }
392 };
393
394 values.push(value);
395 }
396
397 Ok(values)
398 }
399
400 fn decode_column_value(&self, data: &[u8], type_oid: Oid) -> Result<PostgresValue> {
401 let oid_value = type_oid;
403 match oid_value {
404 16 => {
405 Ok(PostgresValue::Bool(data[0] != 0))
407 }
408 21 => {
409 if data.len() == 2 {
411 let mut cursor = Cursor::new(data);
413 Ok(PostgresValue::Int2(cursor.read_i16::<BigEndian>()?))
414 } else {
415 let text = String::from_utf8_lossy(data);
417 let value = text
418 .trim()
419 .parse::<i16>()
420 .map_err(|e| anyhow!("Failed to parse int2 from '{text}': {e}"))?;
421 Ok(PostgresValue::Int2(value))
422 }
423 }
424 23 => {
425 if data.len() == 4 {
427 let mut cursor = Cursor::new(data);
429 Ok(PostgresValue::Int4(cursor.read_i32::<BigEndian>()?))
430 } else {
431 let text = String::from_utf8_lossy(data);
433 let value = text
434 .trim()
435 .parse::<i32>()
436 .map_err(|e| anyhow!("Failed to parse int4 from '{text}': {e}"))?;
437 Ok(PostgresValue::Int4(value))
438 }
439 }
440 20 => {
441 if data.len() == 8 {
443 let mut cursor = Cursor::new(data);
445 Ok(PostgresValue::Int8(cursor.read_i64::<BigEndian>()?))
446 } else {
447 let text = String::from_utf8_lossy(data);
449 let value = text
450 .trim()
451 .parse::<i64>()
452 .map_err(|e| anyhow!("Failed to parse int8 from '{text}': {e}"))?;
453 Ok(PostgresValue::Int8(value))
454 }
455 }
456 700 => {
457 if data.len() == 4 {
459 let mut cursor = Cursor::new(data);
461 Ok(PostgresValue::Float4(cursor.read_f32::<BigEndian>()?))
462 } else {
463 let text = String::from_utf8_lossy(data);
465 let value = text
466 .trim()
467 .parse::<f32>()
468 .map_err(|e| anyhow!("Failed to parse float4 from '{text}': {e}"))?;
469 Ok(PostgresValue::Float4(value))
470 }
471 }
472 701 => {
473 if data.len() == 8 {
475 let mut cursor = Cursor::new(data);
477 Ok(PostgresValue::Float8(cursor.read_f64::<BigEndian>()?))
478 } else {
479 let text = String::from_utf8_lossy(data);
481 let value = text
482 .trim()
483 .parse::<f64>()
484 .map_err(|e| anyhow!("Failed to parse float8 from '{text}': {e}"))?;
485 Ok(PostgresValue::Float8(value))
486 }
487 }
488 1700 => {
489 if data.len() < 8
492 || (!data.is_empty() && data[0] >= b'0' && data[0] <= b'9')
493 || (!data.is_empty() && (data[0] == b'-' || data[0] == b'+' || data[0] == b'.'))
494 {
495 let text = String::from_utf8_lossy(data);
497 let value = Decimal::from_str_exact(text.trim())
498 .map_err(|e| anyhow!("Failed to parse numeric from '{text}': {e}"))?;
499 Ok(PostgresValue::Numeric(value))
500 } else {
501 Ok(PostgresValue::Numeric(decode_numeric(data)?))
503 }
504 }
505 25 | 1043 | 19 => {
506 Ok(PostgresValue::Text(
508 String::from_utf8_lossy(data).to_string(),
509 ))
510 }
511 1042 => {
512 let s = String::from_utf8_lossy(data).trim_end().to_string();
514 Ok(PostgresValue::Char(s))
515 }
516 2950 => {
517 if data.len() != 16 {
519 return Err(anyhow!("Invalid UUID length: {}", data.len()));
520 }
521 let uuid = Uuid::from_slice(data)?;
522 Ok(PostgresValue::Uuid(uuid))
523 }
524 1114 => {
525 if data.len() == 8 {
527 let mut cursor = Cursor::new(data);
529 let micros = cursor.read_i64::<BigEndian>()?;
530 let timestamp = postgres_epoch_to_naive_datetime(micros)?;
531 Ok(PostgresValue::Timestamp(timestamp))
532 } else {
533 let text = String::from_utf8_lossy(data);
535 let timestamp =
536 NaiveDateTime::parse_from_str(text.trim(), "%Y-%m-%d %H:%M:%S%.f")
537 .or_else(|_| {
538 NaiveDateTime::parse_from_str(text.trim(), "%Y-%m-%d %H:%M:%S")
539 })
540 .map_err(|e| anyhow!("Failed to parse timestamp from '{text}': {e}"))?;
541 Ok(PostgresValue::Timestamp(timestamp))
542 }
543 }
544 1184 => {
545 if data.len() == 8 {
547 let mut cursor = Cursor::new(data);
549 let micros = cursor.read_i64::<BigEndian>()?;
550 let timestamp = postgres_epoch_to_datetime(micros)?;
551 Ok(PostgresValue::TimestampTz(timestamp))
552 } else {
553 let text = String::from_utf8_lossy(data);
555 let trimmed = text.trim();
556 let timestamp = DateTime::parse_from_rfc3339(trimmed)
563 .or_else(|_| DateTime::parse_from_str(trimmed, "%Y-%m-%d %H:%M:%S%.f%#z"))
564 .map_err(|e| anyhow!("Failed to parse timestamptz from '{text}': {e}"))?
565 .with_timezone(&Utc);
566 Ok(PostgresValue::TimestampTz(timestamp))
567 }
568 }
569 1082 => {
570 let mut cursor = Cursor::new(data);
572 let days = cursor.read_i32::<BigEndian>()?;
573 let date = postgres_epoch_to_date(days)?;
574 Ok(PostgresValue::Date(date))
575 }
576 1083 => {
577 let mut cursor = Cursor::new(data);
579 let micros = cursor.read_i64::<BigEndian>()?;
580 let time = postgres_time_to_naive_time(micros)?;
581 Ok(PostgresValue::Time(time))
582 }
583 114 | 3802 => {
584 let json_str = if oid_value == 3802 {
586 String::from_utf8_lossy(&data[1..]).to_string()
588 } else {
589 String::from_utf8_lossy(data).to_string()
590 };
591 let value: JsonValue = serde_json::from_str(&json_str)?;
592 Ok(PostgresValue::Json(value))
593 }
594 17 => {
595 Ok(PostgresValue::Bytea(data.to_vec()))
597 }
598 _ => {
599 warn!("Unknown type OID {oid_value}, treating as text");
601 Ok(PostgresValue::Text(
602 String::from_utf8_lossy(data).to_string(),
603 ))
604 }
605 }
606 }
607
608 pub fn get_relation(&self, relation_id: u32) -> Option<&RelationInfo> {
609 self.relations.get(&relation_id)
610 }
611}
612
613fn read_cstring(cursor: &mut Cursor<&[u8]>) -> Result<String> {
614 let mut buffer = Vec::new();
615 loop {
616 let byte = cursor.read_u8()?;
617 if byte == 0 {
618 break;
619 }
620 buffer.push(byte);
621 }
622 Ok(String::from_utf8_lossy(&buffer).to_string())
623}
624
625fn postgres_epoch_to_datetime(micros: i64) -> Result<DateTime<Utc>> {
626 const POSTGRES_EPOCH: i64 = 946684800000000; let unix_micros = micros + POSTGRES_EPOCH;
629 let secs = unix_micros / 1_000_000;
630 let nanos = ((unix_micros % 1_000_000) * 1000) as u32;
631
632 DateTime::from_timestamp(secs, nanos).ok_or_else(|| anyhow!("Invalid timestamp"))
633}
634
635fn postgres_epoch_to_naive_datetime(micros: i64) -> Result<NaiveDateTime> {
636 let dt = postgres_epoch_to_datetime(micros)?;
637 Ok(dt.naive_utc())
638}
639
640fn postgres_epoch_to_date(days: i32) -> Result<chrono::NaiveDate> {
641 const POSTGRES_DATE_EPOCH: i32 = 10957; let unix_days = days + POSTGRES_DATE_EPOCH;
644
645 let epoch =
646 chrono::NaiveDate::from_ymd_opt(1970, 1, 1).ok_or_else(|| anyhow!("Invalid epoch date"))?;
647
648 Ok(epoch + chrono::Duration::days(unix_days as i64))
649}
650
651fn postgres_time_to_naive_time(micros: i64) -> Result<chrono::NaiveTime> {
652 let total_secs = micros / 1_000_000;
653 let hours = (total_secs / 3600) as u32;
654 let minutes = ((total_secs % 3600) / 60) as u32;
655 let seconds = (total_secs % 60) as u32;
656 let nanos = ((micros % 1_000_000) * 1000) as u32;
657
658 chrono::NaiveTime::from_hms_nano_opt(hours, minutes, seconds, nanos)
659 .ok_or_else(|| anyhow!("Invalid time"))
660}
661
662pub fn decode_column_value_text(
664 text: &str,
665 type_oid: i32,
666) -> Result<drasi_core::models::ElementValue> {
667 use drasi_core::models::ElementValue;
668
669 match type_oid as u32 {
670 16 => {
671 let value = text.parse::<bool>().or_else(|_| match text {
673 "t" => Ok(true),
674 "f" => Ok(false),
675 _ => Err(anyhow!("Invalid boolean value")),
676 })?;
677 Ok(ElementValue::Bool(value))
678 }
679 21 => {
680 let value = text.parse::<i16>()?;
682 Ok(ElementValue::Integer(value as i64))
683 }
684 23 => {
685 let value = text.parse::<i32>()?;
687 Ok(ElementValue::Integer(value as i64))
688 }
689 20 => {
690 let value = text.parse::<i64>()?;
692 Ok(ElementValue::Integer(value))
693 }
694 700 => {
695 let value = text.parse::<f32>()?;
697 Ok(ElementValue::Float(ordered_float::OrderedFloat(
698 value as f64,
699 )))
700 }
701 701 => {
702 let value = text.parse::<f64>()?;
704 Ok(ElementValue::Float(ordered_float::OrderedFloat(value)))
705 }
706 1700 => {
707 let value = text.parse::<f64>()?;
709 Ok(ElementValue::Float(ordered_float::OrderedFloat(value)))
710 }
711 25 | 1043 | 19 => {
712 Ok(ElementValue::String(Arc::from(text)))
714 }
715 1114 => {
716 if let Ok(dt) = NaiveDateTime::parse_from_str(text.trim(), "%Y-%m-%d %H:%M:%S%.f") {
721 Ok(ElementValue::LocalDateTime(dt))
722 } else if let Ok(dt) = NaiveDateTime::parse_from_str(text.trim(), "%Y-%m-%d %H:%M:%S") {
723 Ok(ElementValue::LocalDateTime(dt))
724 } else {
725 Ok(ElementValue::String(Arc::from(text)))
727 }
728 }
729 1184 => {
730 if let Ok(dt) = DateTime::parse_from_rfc3339(text.trim()) {
733 Ok(ElementValue::ZonedDateTime(dt.fixed_offset()))
734 } else if let Ok(dt) = DateTime::parse_from_str(text.trim(), "%Y-%m-%d %H:%M:%S%.f%z") {
735 Ok(ElementValue::ZonedDateTime(dt.fixed_offset()))
736 } else if let Ok(dt) =
737 NaiveDateTime::parse_from_str(text.trim(), "%Y-%m-%d %H:%M:%S%.f")
738 {
739 Ok(ElementValue::ZonedDateTime(dt.and_utc().fixed_offset()))
741 } else if let Ok(dt) = NaiveDateTime::parse_from_str(text.trim(), "%Y-%m-%d %H:%M:%S") {
742 Ok(ElementValue::ZonedDateTime(dt.and_utc().fixed_offset()))
744 } else {
745 Ok(ElementValue::String(Arc::from(text)))
747 }
748 }
749 1082 => {
750 Ok(ElementValue::String(Arc::from(text)))
752 }
753 2950 => {
754 Ok(ElementValue::String(Arc::from(text)))
756 }
757 _ => {
758 Ok(ElementValue::String(Arc::from(text)))
760 }
761 }
762}
763
764fn decode_numeric(data: &[u8]) -> Result<Decimal> {
765 if data.len() < 8 {
766 return Err(anyhow!("Numeric data too short"));
767 }
768
769 let mut cursor = Cursor::new(data);
770 let ndigits = cursor.read_u16::<BigEndian>()?;
771 let weight = cursor.read_i16::<BigEndian>()?;
772 let sign = cursor.read_u16::<BigEndian>()?;
773 let dscale = cursor.read_u16::<BigEndian>()?;
774
775 if sign == 0xC000 {
776 return Ok(Decimal::ZERO);
778 }
779
780 let mut digits = Vec::with_capacity(ndigits as usize);
781 for _ in 0..ndigits {
782 digits.push(cursor.read_u16::<BigEndian>()?);
783 }
784
785 let mut result = Decimal::ZERO;
788 let base = Decimal::from(10000);
789
790 for (i, &digit) in digits.iter().enumerate() {
791 let power = weight as i32 - i as i32;
792 let digit_value = Decimal::from(digit as i64);
793 let multiplier = if power >= 0 {
794 let mut result = Decimal::ONE;
795 for _ in 0..power {
796 result *= base;
797 }
798 result
799 } else {
800 let mut result = Decimal::ONE;
801 for _ in 0..(-power) {
802 result /= base;
803 }
804 result
805 };
806 result += digit_value * multiplier;
807 }
808
809 if sign == 0x4000 {
810 result = -result;
811 }
812
813 if dscale > 0 {
815 let mut scale_divisor = Decimal::ONE;
816 for _ in 0..dscale {
817 scale_divisor *= Decimal::from(10);
818 }
819 result /= scale_divisor;
820 }
821
822 Ok(result)
823}
824
825#[cfg(test)]
826mod tests {
827 use super::*;
828 use chrono::NaiveDate;
829 use drasi_core::models::ElementValue;
830
831 #[test]
834 fn decode_timestamp_with_fractional_seconds() {
835 let ev = decode_column_value_text("2024-06-15 10:30:45.123456", 1114).unwrap();
836 let expected = NaiveDate::from_ymd_opt(2024, 6, 15)
837 .unwrap()
838 .and_hms_micro_opt(10, 30, 45, 123456)
839 .unwrap();
840 assert_eq!(ev, ElementValue::LocalDateTime(expected));
841 }
842
843 #[test]
844 fn decode_timestamp_without_fractional_seconds() {
845 let ev = decode_column_value_text("2024-06-15 10:30:45", 1114).unwrap();
846 let expected = NaiveDate::from_ymd_opt(2024, 6, 15)
847 .unwrap()
848 .and_hms_opt(10, 30, 45)
849 .unwrap();
850 assert_eq!(ev, ElementValue::LocalDateTime(expected));
851 }
852
853 #[test]
854 fn decode_timestamp_with_leading_trailing_whitespace() {
855 let ev = decode_column_value_text(" 2024-06-15 10:30:45 ", 1114).unwrap();
856 let expected = NaiveDate::from_ymd_opt(2024, 6, 15)
857 .unwrap()
858 .and_hms_opt(10, 30, 45)
859 .unwrap();
860 assert_eq!(ev, ElementValue::LocalDateTime(expected));
861 }
862
863 #[test]
866 fn decode_timestamptz_rfc3339() {
867 let ev = decode_column_value_text("2024-06-15T10:30:45+01:00", 1184).unwrap();
868 match ev {
869 ElementValue::ZonedDateTime(dt) => {
870 assert_eq!(dt.offset().local_minus_utc(), 3600);
871 assert_eq!(dt.naive_local().to_string(), "2024-06-15 10:30:45");
872 }
873 other => panic!("Expected ZonedDateTime, got {other:?}"),
874 }
875 }
876 use chrono::TimeZone;
877
878 #[test]
879 fn test_decode_timestamptz_text_full_offset() {
880 let decoder = PgOutputDecoder::new();
881 let value = decoder
882 .decode_column_value(b"2026-05-13 22:03:45.423627+00:00", 1184)
883 .expect("should parse timestamptz with full offset");
884 match value {
885 PostgresValue::TimestampTz(ts) => {
886 assert_eq!(
887 ts,
888 Utc.with_ymd_and_hms(2026, 5, 13, 22, 3, 45).unwrap()
889 + chrono::Duration::microseconds(423627)
890 );
891 }
892 other => panic!("expected TimestampTz, got {other:?}"),
893 }
894 }
895
896 #[test]
897 fn decode_timestamptz_with_offset_format() {
898 let ev = decode_column_value_text("2024-06-15 10:30:45.123456+0200", 1184).unwrap();
899 match ev {
900 ElementValue::ZonedDateTime(dt) => {
901 assert_eq!(dt.offset().local_minus_utc(), 7200); }
903 other => panic!("Expected ZonedDateTime, got {other:?}"),
904 }
905 }
906
907 fn test_decode_timestamptz_text_short_offset() {
908 let decoder = PgOutputDecoder::new();
910 let value = decoder
911 .decode_column_value(b"2026-05-13 22:03:45.423627+00", 1184)
912 .expect("should parse timestamptz with short offset");
913 match value {
914 PostgresValue::TimestampTz(ts) => {
915 assert_eq!(
916 ts,
917 Utc.with_ymd_and_hms(2026, 5, 13, 22, 3, 45).unwrap()
918 + chrono::Duration::microseconds(423627)
919 );
920 }
921 other => panic!("expected TimestampTz, got {other:?}"),
922 }
923 }
924
925 #[test]
926 fn decode_timestamptz_utc_via_rfc3339() {
927 let ev = decode_column_value_text("2024-06-15T10:30:45+00:00", 1184).unwrap();
928 match ev {
929 ElementValue::ZonedDateTime(dt) => {
930 assert_eq!(dt.offset().local_minus_utc(), 0);
931 }
932 other => panic!("Expected ZonedDateTime, got {other:?}"),
933 }
934 }
935
936 #[test]
939 fn decode_timestamptz_oid_with_plain_datetime_string_assumes_utc() {
940 let ev = decode_column_value_text("2024-06-15 10:30:45", 1184).unwrap();
943 match ev {
944 ElementValue::ZonedDateTime(dt) => {
945 assert_eq!(dt.offset().local_minus_utc(), 0, "Expected UTC offset");
946 assert_eq!(dt.naive_local().to_string(), "2024-06-15 10:30:45");
947 }
948 other => {
949 panic!("Expected ZonedDateTime for timestamptz OID with no tz info, got {other:?}")
950 }
951 }
952 }
953
954 fn test_decode_timestamptz_text_short_negative_offset() {
955 let decoder = PgOutputDecoder::new();
956 let value = decoder
957 .decode_column_value(b"2026-05-13 22:03:45.423627-05", 1184)
958 .expect("should parse timestamptz with short negative offset");
959 match value {
960 PostgresValue::TimestampTz(ts) => {
961 assert_eq!(
964 ts,
965 Utc.with_ymd_and_hms(2026, 5, 14, 3, 3, 45).unwrap()
966 + chrono::Duration::microseconds(423627)
967 );
968 }
969 other => panic!("expected TimestampTz, got {other:?}"),
970 }
971 }
972
973 #[test]
974 fn test_decode_timestamptz_text_z_suffix() {
975 let decoder = PgOutputDecoder::new();
979 let value = decoder
980 .decode_column_value(b"2026-05-13 22:03:45.423627Z", 1184)
981 .expect("should parse timestamptz with bare Z UTC suffix");
982 match value {
983 PostgresValue::TimestampTz(ts) => {
984 assert_eq!(
985 ts,
986 Utc.with_ymd_and_hms(2026, 5, 13, 22, 3, 45).unwrap()
987 + chrono::Duration::microseconds(423627)
988 );
989 }
990 other => panic!("expected TimestampTz, got {other:?}"),
991 }
992 }
993
994 #[test]
995 fn decode_timestamptz_oid_with_fractional_no_offset_assumes_utc() {
996 let ev = decode_column_value_text("2024-06-15 10:30:45.123456", 1184).unwrap();
998 match ev {
999 ElementValue::ZonedDateTime(dt) => {
1000 assert_eq!(dt.offset().local_minus_utc(), 0, "Expected UTC offset");
1001 }
1002 other => panic!("Expected ZonedDateTime, got {other:?}"),
1003 }
1004 }
1005
1006 #[test]
1007 fn decode_timestamp_oid_with_rfc3339_string() {
1008 let ev = decode_column_value_text("2024-06-15T10:30:45+05:00", 1114).unwrap();
1011 assert!(
1012 matches!(ev, ElementValue::String(_)),
1013 "Expected String fallback for rfc3339 input on timestamp OID, got {ev:?}"
1014 );
1015 }
1016
1017 #[test]
1018 fn decode_unparseable_timestamp_falls_back_to_string() {
1019 let ev = decode_column_value_text("not-a-date", 1114).unwrap();
1020 assert!(
1021 matches!(ev, ElementValue::String(_)),
1022 "Expected String fallback, got {ev:?}"
1023 );
1024 }
1025
1026 #[test]
1029 fn decode_bool_text() {
1030 assert_eq!(
1031 decode_column_value_text("true", 16).unwrap(),
1032 ElementValue::Bool(true)
1033 );
1034 assert_eq!(
1035 decode_column_value_text("t", 16).unwrap(),
1036 ElementValue::Bool(true)
1037 );
1038 assert_eq!(
1039 decode_column_value_text("f", 16).unwrap(),
1040 ElementValue::Bool(false)
1041 );
1042 }
1043
1044 #[test]
1045 fn decode_int4_text() {
1046 assert_eq!(
1047 decode_column_value_text("42", 23).unwrap(),
1048 ElementValue::Integer(42)
1049 );
1050 }
1051
1052 #[test]
1053 fn decode_text_type() {
1054 assert_eq!(
1055 decode_column_value_text("hello world", 25).unwrap(),
1056 ElementValue::String(Arc::from("hello world"))
1057 );
1058 }
1059 fn test_decode_timestamptz_text_no_fractional_seconds() {
1060 let decoder = PgOutputDecoder::new();
1063 let value = decoder
1064 .decode_column_value(b"2026-05-13 22:03:45+00", 1184)
1065 .expect("should parse timestamptz without fractional seconds");
1066 match value {
1067 PostgresValue::TimestampTz(ts) => {
1068 assert_eq!(ts, Utc.with_ymd_and_hms(2026, 5, 13, 22, 3, 45).unwrap());
1069 }
1070 other => panic!("expected TimestampTz, got {other:?}"),
1071 }
1072 }
1073}