1use std::sync::Arc;
7
8use rapidhash::quality::RapidHasher;
9
10use crate::DriverError;
11use crate::arena::Arena;
12
13#[derive(Debug, Clone)]
20pub struct Config {
21 pub host: String,
22 pub port: u16,
23 pub user: String,
24 pub password: String,
25 pub database: String,
26 pub ssl: SslMode,
27 pub statement_timeout_secs: u32,
32}
33
34impl Drop for Config {
36 fn drop(&mut self) {
37 use zeroize::Zeroize;
38 self.password.zeroize();
39 }
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum SslMode {
45 Disable,
47 Prefer,
49 Require,
51}
52
53impl Config {
54 pub fn from_url(url: &str) -> Result<Self, DriverError> {
68 let url = url
69 .strip_prefix("postgres://")
70 .or_else(|| url.strip_prefix("postgresql://"))
71 .ok_or_else(|| DriverError::Protocol("URL must start with postgres://".into()))?;
72
73 let (userinfo, rest) = url
75 .split_once('@')
76 .ok_or_else(|| DriverError::Protocol("missing @ in connection URL".into()))?;
77
78 let (user, password) = userinfo.split_once(':').unwrap_or((userinfo, ""));
79
80 let (hostport, rest) = rest.split_once('/').unwrap_or((rest, ""));
82 let (database, params) = rest.split_once('?').unwrap_or((rest, ""));
83
84 let (host, port) = if let Some((h, p)) = hostport.split_once(':') {
85 let port = p
86 .parse::<u16>()
87 .map_err(|_| DriverError::Protocol(format!("invalid port: {p}")))?;
88 (h.to_owned(), port)
89 } else {
90 (hostport.to_owned(), 5432)
91 };
92
93 let mut ssl = SslMode::Prefer;
94 let mut statement_timeout_secs: u32 = 30;
95 let mut host_override: Option<String> = None;
96 for param in params.split('&') {
97 if param.is_empty() {
98 continue;
99 }
100 if let Some(val) = param.strip_prefix("sslmode=") {
101 ssl = match val {
103 "disable" => SslMode::Disable,
104 "prefer" => SslMode::Prefer,
105 "require" => SslMode::Require,
106 _ => {
107 return Err(DriverError::Protocol(format!(
108 "unknown sslmode: '{val}' (expected: disable, prefer, require)"
109 )));
110 }
111 };
112 } else if let Some(val) = param.strip_prefix("statement_timeout=") {
113 statement_timeout_secs = val.parse::<u32>().unwrap_or(30);
114 } else if let Some(val) = param.strip_prefix("host=") {
115 host_override = Some(url_decode(val)?);
116 }
117 }
118
119 let final_host = if let Some(h) = host_override {
122 h
123 } else {
124 url_decode(&host)?
125 };
126
127 let config = Config {
128 host: final_host,
129 port,
130 user: url_decode(user)?,
131 password: url_decode(password)?,
132 database: if database.is_empty() {
133 url_decode(user)?
134 } else {
135 url_decode(database)?
136 },
137 ssl,
138 statement_timeout_secs,
139 };
140 config.validate()?;
141 Ok(config)
142 }
143
144 pub fn validate(&self) -> Result<(), DriverError> {
149 if self.host.is_empty() {
150 return Err(DriverError::Protocol("host cannot be empty".into()));
151 }
152 if self.user.is_empty() {
153 return Err(DriverError::Protocol("user cannot be empty".into()));
154 }
155 if self.database.is_empty() {
156 return Err(DriverError::Protocol("database cannot be empty".into()));
157 }
158 Ok(())
159 }
160
161 pub fn host_is_uds(&self) -> bool {
166 self.host.starts_with('/')
167 }
168
169 pub fn uds_path(&self) -> String {
173 format!("{}/.s.PGSQL.{}", self.host, self.port)
174 }
175}
176
177fn url_decode(s: &str) -> Result<String, DriverError> {
187 let mut bytes = Vec::with_capacity(s.len());
188 let input = s.as_bytes();
189 let mut i = 0;
190 while i < input.len() {
191 if input[i] == b'%' {
192 if i + 2 >= input.len() {
193 return Err(DriverError::Protocol(format!(
194 "malformed percent-encoding in URL: '{s}'"
195 )));
196 }
197 let hi = hex_val(input[i + 1]).ok_or_else(|| {
198 DriverError::Protocol(format!(
199 "invalid hex digit '{}' in URL: '{s}'",
200 input[i + 1] as char
201 ))
202 })?;
203 let lo = hex_val(input[i + 2]).ok_or_else(|| {
204 DriverError::Protocol(format!(
205 "invalid hex digit '{}' in URL: '{s}'",
206 input[i + 2] as char
207 ))
208 })?;
209 bytes.push(hi * 16 + lo);
210 i += 3;
211 } else {
212 bytes.push(input[i]);
213 i += 1;
214 }
215 }
216 String::from_utf8(bytes)
217 .map_err(|_| DriverError::Protocol(format!("invalid UTF-8 in URL: '{s}'")))
218}
219
220fn hex_val(b: u8) -> Option<u8> {
221 match b {
222 b'0'..=b'9' => Some(b - b'0'),
223 b'a'..=b'f' => Some(b - b'a' + 10),
224 b'A'..=b'F' => Some(b - b'A' + 10),
225 _ => None,
226 }
227}
228
229pub(crate) enum StartupAction {
235 AuthOk,
236 AuthCleartext,
237 AuthMd5([u8; 4]),
238 AuthSasl(Vec<u8>),
239 ParameterStatus(Box<str>, Box<str>),
240 BackendKeyData(i32, i32),
241 ReadyForQuery(u8),
242 Error(String),
243 Notice,
244}
245
246#[derive(Debug, Clone)]
252pub struct ColumnDesc {
253 pub name: Box<str>,
255 pub type_oid: u32,
257 pub type_size: i16,
259 pub table_oid: u32,
261 pub column_id: i16,
263}
264
265#[derive(Debug, Clone)]
268pub struct PrepareResult {
269 pub columns: Vec<ColumnDesc>,
271 pub param_oids: Vec<u32>,
273}
274
275pub type SimpleRow = Vec<Option<String>>;
280
281#[derive(Debug, Clone)]
287pub struct Notification {
288 pub pid: i32,
290 pub channel: String,
292 pub payload: String,
294}
295
296pub struct QueryResult {
313 pub(crate) all_col_offsets: Vec<(usize, i32)>,
317 pub(crate) num_cols: usize,
319 pub(crate) columns: Arc<[ColumnDesc]>,
320 pub(crate) affected_rows: u64,
321 pub(crate) data_buf: Option<Vec<u8>>,
325}
326
327impl QueryResult {
328 pub fn from_parts(
332 all_col_offsets: Vec<(usize, i32)>,
333 num_cols: usize,
334 columns: Arc<[ColumnDesc]>,
335 affected_rows: u64,
336 ) -> Self {
337 Self {
338 all_col_offsets,
339 num_cols,
340 columns,
341 affected_rows,
342 data_buf: None,
343 }
344 }
345
346 pub fn from_parts_with_buf(
348 all_col_offsets: Vec<(usize, i32)>,
349 num_cols: usize,
350 columns: Arc<[ColumnDesc]>,
351 affected_rows: u64,
352 data_buf: Vec<u8>,
353 ) -> Self {
354 Self {
355 all_col_offsets,
356 num_cols,
357 columns,
358 affected_rows,
359 data_buf: if data_buf.is_empty() {
360 None
361 } else {
362 Some(data_buf)
363 },
364 }
365 }
366
367 pub fn len(&self) -> usize {
369 if self.num_cols == 0 {
370 return 0;
371 }
372 self.all_col_offsets.len() / self.num_cols
373 }
374
375 pub fn is_empty(&self) -> bool {
377 self.all_col_offsets.is_empty()
378 }
379
380 pub fn affected_rows(&self) -> u64 {
382 self.affected_rows
383 }
384
385 pub fn columns(&self) -> &[ColumnDesc] {
387 &self.columns
388 }
389
390 pub fn row<'a>(&'a self, idx: usize, arena: &'a Arena) -> Row<'a> {
393 let start = idx * self.num_cols;
394 let end = start + self.num_cols;
395 Row {
396 data: self.data_buf.as_deref(),
397 arena,
398 col_offsets: &self.all_col_offsets[start..end],
399 columns: &self.columns,
400 }
401 }
402
403 pub fn take_col_offsets(&mut self) -> Vec<(usize, i32)> {
408 std::mem::take(&mut self.all_col_offsets)
409 }
410
411 pub fn take_data_buf(&mut self) -> Option<Vec<u8>> {
413 self.data_buf.take()
414 }
415
416 pub fn rows<'a>(&'a self, arena: &'a Arena) -> impl Iterator<Item = Row<'a>> {
418 let num_cols = self.num_cols;
419 let columns = &self.columns;
420 let data = self.data_buf.as_deref();
421 self.all_col_offsets
422 .chunks(num_cols.max(1))
423 .map(move |chunk| Row {
424 data,
425 arena,
426 col_offsets: chunk,
427 columns,
428 })
429 }
430}
431
432pub struct Row<'a> {
443 data: Option<&'a [u8]>,
446 arena: &'a Arena,
447 col_offsets: &'a [(usize, i32)],
448 columns: &'a [ColumnDesc],
449}
450
451impl<'a> Row<'a> {
452 pub fn get_raw(&self, idx: usize) -> Option<&'a [u8]> {
454 let (offset, len) = self.col_offsets[idx];
455 if len < 0 {
456 None
457 } else if let Some(buf) = self.data {
458 Some(&buf[offset..offset + len as usize])
459 } else {
460 Some(self.arena.get(offset, len as usize))
461 }
462 }
463
464 pub fn is_null(&self, idx: usize) -> bool {
466 self.col_offsets[idx].1 < 0
467 }
468
469 pub fn column_count(&self) -> usize {
471 self.col_offsets.len()
472 }
473
474 pub fn get_bool(&self, idx: usize) -> Option<bool> {
476 self.get_raw(idx)
477 .and_then(|data| crate::codec::decode_bool(data).ok())
478 }
479
480 pub fn get_i16(&self, idx: usize) -> Option<i16> {
482 self.get_raw(idx)
483 .and_then(|data| crate::codec::decode_i16(data).ok())
484 }
485
486 pub fn get_i32(&self, idx: usize) -> Option<i32> {
488 self.get_raw(idx)
489 .and_then(|data| crate::codec::decode_i32(data).ok())
490 }
491
492 pub fn get_i64(&self, idx: usize) -> Option<i64> {
494 self.get_raw(idx)
495 .and_then(|data| crate::codec::decode_i64(data).ok())
496 }
497
498 pub fn get_f32(&self, idx: usize) -> Option<f32> {
500 self.get_raw(idx)
501 .and_then(|data| crate::codec::decode_f32(data).ok())
502 }
503
504 pub fn get_f64(&self, idx: usize) -> Option<f64> {
506 self.get_raw(idx)
507 .and_then(|data| crate::codec::decode_f64(data).ok())
508 }
509
510 pub fn get_str(&self, idx: usize) -> Option<&'a str> {
512 self.get_raw(idx)
513 .and_then(|data| crate::codec::decode_str(data).ok())
514 }
515
516 pub fn get_bytes(&self, idx: usize) -> Option<&'a [u8]> {
518 self.get_raw(idx)
519 }
520
521 pub fn column_name(&self, idx: usize) -> &str {
523 &self.columns[idx].name
524 }
525
526 pub fn column_type_oid(&self, idx: usize) -> u32 {
528 self.columns[idx].type_oid
529 }
530}
531
532pub struct PgDataRow<'a> {
545 data: &'a [u8],
546 offsets: smallvec::SmallVec<[(usize, i32); 16]>,
549}
550
551impl<'a> PgDataRow<'a> {
552 pub fn new(data: &'a [u8]) -> Result<Self, DriverError> {
557 if data.len() < 2 {
558 return Err(DriverError::Protocol("DataRow too short".into()));
559 }
560 let num_cols = i16::from_be_bytes([data[0], data[1]]);
561 if num_cols < 0 {
562 return Err(DriverError::Protocol(
563 "DataRow: negative column count".into(),
564 ));
565 }
566 let num_cols = num_cols as usize;
567 let mut offsets = smallvec::SmallVec::<[(usize, i32); 16]>::with_capacity(num_cols);
568 let mut pos = 2usize;
569 for _ in 0..num_cols {
570 if pos + 4 > data.len() {
571 return Err(DriverError::Protocol("DataRow truncated".into()));
572 }
573 let col_len =
574 i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
575 pos += 4;
576 offsets.push((pos, col_len));
577 if col_len > 0 {
578 pos += col_len as usize;
579 }
580 }
581 Ok(Self { data, offsets })
582 }
583
584 #[inline]
586 pub fn get_raw(&self, idx: usize) -> Option<&'a [u8]> {
587 let (offset, len) = self.offsets[idx];
588 if len < 0 {
589 None
590 } else {
591 Some(&self.data[offset..offset + len as usize])
592 }
593 }
594
595 #[inline]
597 pub fn is_null(&self, idx: usize) -> bool {
598 self.offsets[idx].1 < 0
599 }
600
601 #[inline]
603 pub fn column_count(&self) -> usize {
604 self.offsets.len()
605 }
606
607 #[inline]
609 pub fn get_bool(&self, idx: usize) -> Option<bool> {
610 self.get_raw(idx)
611 .and_then(|data| crate::codec::decode_bool(data).ok())
612 }
613
614 #[inline]
616 pub fn get_i16(&self, idx: usize) -> Option<i16> {
617 self.get_raw(idx)
618 .and_then(|data| crate::codec::decode_i16(data).ok())
619 }
620
621 #[inline]
623 pub fn get_i32(&self, idx: usize) -> Option<i32> {
624 self.get_raw(idx)
625 .and_then(|data| crate::codec::decode_i32(data).ok())
626 }
627
628 #[inline]
630 pub fn get_i64(&self, idx: usize) -> Option<i64> {
631 self.get_raw(idx)
632 .and_then(|data| crate::codec::decode_i64(data).ok())
633 }
634
635 #[inline]
637 pub fn get_f32(&self, idx: usize) -> Option<f32> {
638 self.get_raw(idx)
639 .and_then(|data| crate::codec::decode_f32(data).ok())
640 }
641
642 #[inline]
644 pub fn get_f64(&self, idx: usize) -> Option<f64> {
645 self.get_raw(idx)
646 .and_then(|data| crate::codec::decode_f64(data).ok())
647 }
648
649 #[inline]
651 pub fn get_str(&self, idx: usize) -> Option<&'a str> {
652 self.get_raw(idx)
653 .and_then(|data| crate::codec::decode_str(data).ok())
654 }
655
656 #[inline]
658 pub fn get_bytes(&self, idx: usize) -> Option<&'a [u8]> {
659 self.get_raw(idx)
660 }
661}
662
663pub fn hash_sql(sql: &str) -> u64 {
671 use std::hash::{Hash, Hasher};
672 let mut hasher = RapidHasher::default();
673 sql.hash(&mut hasher);
674 hasher.finish()
675}
676
677#[cfg(test)]
682#[allow(clippy::approx_constant)]
683mod tests {
684 use super::*;
685
686 #[test]
691 fn config_parse_full_url() {
692 let cfg = Config::from_url("postgres://user:pass@localhost:5432/mydb").unwrap();
693 assert_eq!(cfg.user, "user");
694 assert_eq!(cfg.password, "pass");
695 assert_eq!(cfg.host, "localhost");
696 assert_eq!(cfg.port, 5432);
697 assert_eq!(cfg.database, "mydb");
698 }
699
700 #[test]
701 fn config_parse_default_port() {
702 let cfg = Config::from_url("postgres://user:pass@localhost/mydb").unwrap();
703 assert_eq!(cfg.port, 5432);
704 }
705
706 #[test]
707 fn config_parse_no_password() {
708 let cfg = Config::from_url("postgres://user@localhost/mydb").unwrap();
709 assert_eq!(cfg.user, "user");
710 assert_eq!(cfg.password, "");
711 }
712
713 #[test]
714 fn config_parse_empty_database() {
715 let cfg = Config::from_url("postgres://user:pass@localhost").unwrap();
716 assert_eq!(cfg.database, "user");
718 }
719
720 #[test]
721 fn config_parse_sslmode() {
722 let cfg = Config::from_url("postgres://user:pass@localhost/db?sslmode=require").unwrap();
723 assert_eq!(cfg.ssl, SslMode::Require);
724 }
725
726 #[test]
727 fn config_parse_percent_encoding() {
728 let cfg = Config::from_url("postgres://user%40domain:p%40ss@localhost/db").unwrap();
729 assert_eq!(cfg.user, "user@domain");
730 assert_eq!(cfg.password, "p@ss");
731 }
732
733 #[test]
734 fn config_rejects_bad_scheme() {
735 let result = Config::from_url("mysql://user:pass@localhost/db");
736 assert!(result.is_err());
737 }
738
739 #[test]
741 fn config_rejects_unknown_sslmode() {
742 let result = Config::from_url("postgres://user:pass@localhost/db?sslmode=requre");
743 assert!(result.is_err(), "typo 'requre' should be rejected");
744 let result = Config::from_url("postgres://user:pass@localhost/db?sslmode=REQUIRE");
745 assert!(result.is_err(), "uppercase should be rejected");
746 let result = Config::from_url("postgres://user:pass@localhost/db?sslmode=bogus");
747 assert!(result.is_err(), "bogus value should be rejected");
748 }
749
750 #[test]
752 fn config_accepts_valid_sslmodes() {
753 let cfg = Config::from_url("postgres://user:pass@localhost/db?sslmode=disable").unwrap();
754 assert_eq!(cfg.ssl, SslMode::Disable);
755 let cfg = Config::from_url("postgres://user:pass@localhost/db?sslmode=prefer").unwrap();
756 assert_eq!(cfg.ssl, SslMode::Prefer);
757 let cfg = Config::from_url("postgres://user:pass@localhost/db?sslmode=require").unwrap();
758 assert_eq!(cfg.ssl, SslMode::Require);
759 }
760
761 #[test]
763 fn config_parse_postgresql_scheme() {
764 let cfg = Config::from_url("postgresql://user:pass@localhost:5432/mydb").unwrap();
765 assert_eq!(cfg.user, "user");
766 assert_eq!(cfg.password, "pass");
767 assert_eq!(cfg.host, "localhost");
768 assert_eq!(cfg.port, 5432);
769 assert_eq!(cfg.database, "mydb");
770 }
771
772 #[test]
774 fn config_parse_no_password_standalone() {
775 let cfg = Config::from_url("postgres://admin@db.example.com/myapp").unwrap();
776 assert_eq!(cfg.user, "admin");
777 assert_eq!(cfg.password, "");
778 assert_eq!(cfg.host, "db.example.com");
779 assert_eq!(cfg.database, "myapp");
780 }
781
782 #[test]
784 fn config_empty_database_falls_back_to_user() {
785 let cfg = Config::from_url("postgres://testuser:pass@localhost").unwrap();
786 assert_eq!(cfg.database, "testuser");
787 }
788
789 #[test]
791 fn config_unknown_sslmode_error() {
792 let result = Config::from_url("postgres://u:p@h/d?sslmode=verify-full");
793 assert!(result.is_err());
794 let err = result.unwrap_err().to_string();
795 assert!(
796 err.contains("unknown sslmode"),
797 "should describe unknown sslmode: {err}"
798 );
799 }
800
801 #[test]
803 fn config_multiple_query_params() {
804 let cfg = Config::from_url(
805 "postgres://user:pass@localhost/db?sslmode=disable&statement_timeout=60",
806 )
807 .unwrap();
808 assert_eq!(cfg.ssl, SslMode::Disable);
809 assert_eq!(cfg.statement_timeout_secs, 60);
810 }
811
812 #[test]
814 fn config_validate_empty_host() {
815 let cfg = Config {
816 host: String::new(),
817 port: 5432,
818 user: "user".into(),
819 password: "pass".into(),
820 database: "db".into(),
821 ssl: SslMode::Disable,
822 statement_timeout_secs: 30,
823 };
824 assert!(cfg.validate().is_err());
825 }
826
827 #[test]
829 fn config_validate_empty_user() {
830 let cfg = Config {
831 host: "localhost".into(),
832 port: 5432,
833 user: String::new(),
834 password: "pass".into(),
835 database: "db".into(),
836 ssl: SslMode::Disable,
837 statement_timeout_secs: 30,
838 };
839 assert!(cfg.validate().is_err());
840 }
841
842 #[test]
844 fn config_validate_empty_database() {
845 let cfg = Config {
846 host: "localhost".into(),
847 port: 5432,
848 user: "user".into(),
849 password: "pass".into(),
850 database: String::new(),
851 ssl: SslMode::Disable,
852 statement_timeout_secs: 30,
853 };
854 assert!(cfg.validate().is_err());
855 }
856
857 #[test]
859 fn config_missing_at_sign() {
860 let result = Config::from_url("postgres://userpasslocalhost/db");
861 assert!(result.is_err());
862 }
863
864 #[test]
866 fn config_custom_port() {
867 let cfg = Config::from_url("postgres://user:pass@localhost:5433/db").unwrap();
868 assert_eq!(cfg.port, 5433);
869 }
870
871 #[test]
873 fn config_invalid_port() {
874 let result = Config::from_url("postgres://user:pass@localhost:notaport/db");
875 assert!(result.is_err());
876 }
877
878 #[cfg(not(feature = "tls"))]
880 #[test]
881 fn config_sslmode_require_without_tls_feature() {
882 let cfg = Config::from_url("postgres://user:pass@localhost/db?sslmode=require").unwrap();
885 assert_eq!(cfg.ssl, SslMode::Require);
886 }
887
888 #[test]
889 fn config_statement_timeout_default() {
890 let cfg = Config::from_url("postgres://user:pass@localhost/db").unwrap();
891 assert_eq!(cfg.statement_timeout_secs, 30);
892 }
893
894 #[test]
895 fn config_statement_timeout_custom() {
896 let cfg =
897 Config::from_url("postgres://user:pass@localhost/db?statement_timeout=120").unwrap();
898 assert_eq!(cfg.statement_timeout_secs, 120);
899 }
900
901 #[test]
902 fn config_statement_timeout_zero() {
903 let cfg =
904 Config::from_url("postgres://user:pass@localhost/db?statement_timeout=0").unwrap();
905 assert_eq!(cfg.statement_timeout_secs, 0);
906 }
907
908 #[test]
909 fn config_statement_timeout_invalid_falls_back() {
910 let cfg =
911 Config::from_url("postgres://user:pass@localhost/db?statement_timeout=notanumber")
912 .unwrap();
913 assert_eq!(cfg.statement_timeout_secs, 30); }
915
916 #[test]
917 fn config_uds_path_format() {
918 let cfg = Config::from_url("postgres://user@localhost/db?host=/tmp").unwrap();
919 assert_eq!(cfg.uds_path(), "/tmp/.s.PGSQL.5432");
920 }
921
922 #[test]
923 fn config_uds_path_custom_port() {
924 let cfg = Config::from_url("postgres://user@localhost:5433/db?host=/tmp").unwrap();
925 assert_eq!(cfg.uds_path(), "/tmp/.s.PGSQL.5433");
926 }
927
928 #[test]
933 fn config_host_is_uds_absolute_path() {
934 let cfg = Config {
935 host: "/tmp".into(),
936 port: 5432,
937 user: "user".into(),
938 password: "".into(),
939 database: "db".into(),
940 ssl: SslMode::Disable,
941 statement_timeout_secs: 30,
942 };
943 assert!(cfg.host_is_uds());
944 assert_eq!(cfg.uds_path(), "/tmp/.s.PGSQL.5432");
945 }
946
947 #[test]
948 fn config_host_is_uds_var_run() {
949 let cfg = Config {
950 host: "/var/run/postgresql".into(),
951 port: 5433,
952 user: "user".into(),
953 password: "".into(),
954 database: "db".into(),
955 ssl: SslMode::Disable,
956 statement_timeout_secs: 30,
957 };
958 assert!(cfg.host_is_uds());
959 assert_eq!(cfg.uds_path(), "/var/run/postgresql/.s.PGSQL.5433");
960 }
961
962 #[test]
963 fn config_host_is_not_uds_for_hostname() {
964 let cfg = Config {
965 host: "localhost".into(),
966 port: 5432,
967 user: "user".into(),
968 password: "".into(),
969 database: "db".into(),
970 ssl: SslMode::Disable,
971 statement_timeout_secs: 30,
972 };
973 assert!(!cfg.host_is_uds());
974 }
975
976 #[test]
977 fn config_host_is_not_uds_for_ip() {
978 let cfg = Config {
979 host: "127.0.0.1".into(),
980 port: 5432,
981 user: "user".into(),
982 password: "".into(),
983 database: "db".into(),
984 ssl: SslMode::Disable,
985 statement_timeout_secs: 30,
986 };
987 assert!(!cfg.host_is_uds());
988 }
989
990 #[test]
991 fn config_parse_uds_host_query_param() {
992 let cfg = Config::from_url("postgres://user@localhost/mydb?host=/tmp").unwrap();
993 assert_eq!(cfg.host, "/tmp");
994 assert!(cfg.host_is_uds());
995 assert_eq!(cfg.uds_path(), "/tmp/.s.PGSQL.5432");
996 assert_eq!(cfg.database, "mydb");
997 assert_eq!(cfg.user, "user");
998 }
999
1000 #[test]
1001 fn config_parse_uds_host_query_param_custom_port() {
1002 let cfg = Config::from_url("postgres://user@localhost:5433/mydb?host=/var/run/postgresql")
1003 .unwrap();
1004 assert_eq!(cfg.host, "/var/run/postgresql");
1005 assert_eq!(cfg.port, 5433);
1006 assert_eq!(cfg.uds_path(), "/var/run/postgresql/.s.PGSQL.5433");
1007 }
1008
1009 #[test]
1010 fn config_parse_uds_host_with_other_params() {
1011 let cfg = Config::from_url(
1012 "postgres://user@localhost/db?host=/tmp&sslmode=disable&statement_timeout=60",
1013 )
1014 .unwrap();
1015 assert_eq!(cfg.host, "/tmp");
1016 assert!(cfg.host_is_uds());
1017 assert_eq!(cfg.ssl, SslMode::Disable);
1018 assert_eq!(cfg.statement_timeout_secs, 60);
1019 }
1020
1021 #[test]
1022 fn config_parse_uds_host_percent_encoded() {
1023 let cfg = Config::from_url("postgres://user@localhost/db?host=%2Ftmp").unwrap();
1025 assert_eq!(cfg.host, "/tmp");
1026 assert!(cfg.host_is_uds());
1027 }
1028
1029 #[test]
1030 fn config_parse_tcp_host_not_overridden_without_param() {
1031 let cfg = Config::from_url("postgres://user@myserver/db").unwrap();
1033 assert_eq!(cfg.host, "myserver");
1034 assert!(!cfg.host_is_uds());
1035 }
1036
1037 #[test]
1038 fn config_parse_uds_host_overrides_url_hostname() {
1039 let cfg = Config::from_url("postgres://user@db.example.com/mydb?host=/var/run/postgresql")
1041 .unwrap();
1042 assert_eq!(cfg.host, "/var/run/postgresql");
1043 assert!(cfg.host_is_uds());
1044 }
1045
1046 #[test]
1047 fn config_parse_uds_empty_url_host() {
1048 let cfg = Config::from_url("postgres://user@/mydb?host=/tmp").unwrap();
1050 assert_eq!(cfg.host, "/tmp");
1051 assert!(cfg.host_is_uds());
1052 assert_eq!(cfg.database, "mydb");
1053 }
1054
1055 #[test]
1060 fn url_decode_works() {
1061 assert_eq!(url_decode("hello%20world").unwrap(), "hello world");
1062 assert_eq!(url_decode("no%20escape").unwrap(), "no escape");
1063 assert_eq!(url_decode("plain").unwrap(), "plain");
1064 assert_eq!(url_decode("a%40b").unwrap(), "a@b");
1065 }
1066
1067 #[test]
1068 fn url_decode_malformed_percent_trailing() {
1069 let result = url_decode("abc%2");
1071 assert!(result.is_err(), "truncated %2 should error");
1072 }
1073
1074 #[test]
1075 fn url_decode_malformed_percent_no_digits() {
1076 let result = url_decode("abc%");
1078 assert!(result.is_err(), "bare % at end should error");
1079 }
1080
1081 #[test]
1082 fn url_decode_invalid_hex_digit() {
1083 let result = url_decode("abc%GG");
1085 assert!(result.is_err(), "%GG should error");
1086 }
1087
1088 #[test]
1089 fn url_decode_invalid_hex_second_digit() {
1090 let result = url_decode("abc%2Z");
1092 assert!(result.is_err(), "%2Z should error");
1093 }
1094
1095 #[test]
1097 fn url_decode_invalid_utf8_percent() {
1098 let result = url_decode("%80%81");
1100 assert!(result.is_err(), "invalid UTF-8 bytes should error");
1101 }
1102
1103 #[test]
1105 fn url_decode_percent_everywhere() {
1106 assert_eq!(url_decode("%41%42%43").unwrap(), "ABC");
1107 assert_eq!(url_decode("%61").unwrap(), "a");
1108 assert_eq!(url_decode("x%2Fy%2Fz").unwrap(), "x/y/z");
1109 }
1110
1111 #[test]
1113 fn url_decode_bare_percent_middle() {
1114 assert!(url_decode("a%b").is_err(), "bare % in middle should error");
1115 }
1116
1117 #[test]
1119 fn url_decode_multibyte_utf8() {
1120 let result = url_decode("caf%C3%A9").unwrap();
1121 assert_eq!(result, "caf\u{00e9}"); }
1123
1124 #[test]
1126 fn url_decode_invalid_percent_zz() {
1127 let result = url_decode("abc%ZZ");
1128 assert!(result.is_err(), "%ZZ should error");
1129 }
1130
1131 #[test]
1133 fn url_decode_truncated_percent_trailing() {
1134 let result = url_decode("abc%");
1135 assert!(result.is_err(), "trailing % should error");
1136 }
1137
1138 #[test]
1140 fn url_decode_invalid_utf8() {
1141 let result = url_decode("%80");
1143 assert!(result.is_err(), "invalid UTF-8 should error");
1144 }
1145
1146 #[test]
1147 fn url_decode_empty_string() {
1148 assert_eq!(url_decode("").unwrap(), "");
1149 }
1150
1151 #[test]
1152 fn url_decode_no_encoding() {
1153 assert_eq!(url_decode("hello").unwrap(), "hello");
1154 }
1155
1156 #[test]
1157 fn url_decode_all_ascii_hex() {
1158 assert_eq!(url_decode("%2F").unwrap(), "/");
1160 assert_eq!(url_decode("%2f").unwrap(), "/");
1161 }
1162
1163 #[test]
1167 fn config_unicode_password() {
1168 let cfg =
1170 Config::from_url("postgres://user:%D0%BF%D0%B0%D1%80%D0%BE%D0%BB%D1%8C@localhost/db")
1171 .unwrap();
1172 assert_eq!(cfg.user, "user");
1173 assert_eq!(
1174 cfg.password,
1175 "\u{043F}\u{0430}\u{0440}\u{043E}\u{043B}\u{044C}"
1176 ); assert_eq!(cfg.host, "localhost");
1178 assert_eq!(cfg.database, "db");
1179 }
1180
1181 #[test]
1183 fn config_port_zero() {
1184 let cfg = Config::from_url("postgres://user:pass@localhost:0/db").unwrap();
1185 assert_eq!(cfg.port, 0);
1186 }
1187
1188 #[test]
1190 fn config_port_max() {
1191 let cfg = Config::from_url("postgres://user:pass@localhost:65535/db").unwrap();
1192 assert_eq!(cfg.port, 65535);
1193 }
1194
1195 #[test]
1197 fn config_port_overflow() {
1198 let result = Config::from_url("postgres://user:pass@localhost:65536/db");
1199 assert!(result.is_err(), "port 65536 exceeds u16 max");
1200 }
1201
1202 #[test]
1204 fn config_unknown_param_ignored() {
1205 let cfg = Config::from_url(
1206 "postgres://user:pass@localhost/db?application_name=myapp&connect_timeout=10",
1207 )
1208 .unwrap();
1209 assert_eq!(cfg.user, "user");
1211 assert_eq!(cfg.host, "localhost");
1212 assert_eq!(cfg.database, "db");
1213 assert_eq!(cfg.statement_timeout_secs, 30);
1215 assert_eq!(cfg.ssl, SslMode::Prefer);
1216 }
1217
1218 #[test]
1220 fn url_decode_double_percent_encoding() {
1221 assert_eq!(url_decode("%2525").unwrap(), "%25");
1223 }
1224
1225 #[test]
1227 fn config_explicit_empty_password() {
1228 let cfg = Config::from_url("postgres://user:@localhost/db").unwrap();
1229 assert_eq!(cfg.user, "user");
1230 assert_eq!(cfg.password, "");
1231 }
1232
1233 #[test]
1235 fn config_special_chars_in_user() {
1236 let cfg = Config::from_url("postgres://my%2Fuser:pass@localhost/my%2Fdb").unwrap();
1237 assert_eq!(cfg.user, "my/user");
1238 assert_eq!(cfg.database, "my/db");
1239 }
1240
1241 #[test]
1243 fn url_decode_plus_is_literal() {
1244 assert_eq!(url_decode("a+b").unwrap(), "a+b");
1245 }
1246
1247 #[test]
1249 fn config_minimal_valid_url() {
1250 let cfg = Config::from_url("postgres://user@localhost/db").unwrap();
1251 assert_eq!(cfg.user, "user");
1252 assert_eq!(cfg.password, "");
1253 assert_eq!(cfg.host, "localhost");
1254 assert_eq!(cfg.port, 5432);
1255 assert_eq!(cfg.database, "db");
1256 }
1257
1258 #[test]
1260 fn config_empty_param_segments() {
1261 let cfg =
1262 Config::from_url("postgres://user:pass@localhost/db?&&statement_timeout=60&&").unwrap();
1263 assert_eq!(cfg.statement_timeout_secs, 60);
1264 }
1265
1266 #[test]
1271 fn hash_sql_deterministic() {
1272 let h1 = hash_sql("SELECT 1");
1273 let h2 = hash_sql("SELECT 1");
1274 assert_eq!(h1, h2);
1275 }
1276
1277 #[test]
1278 fn hash_sql_different_queries() {
1279 let h1 = hash_sql("SELECT 1");
1280 let h2 = hash_sql("SELECT 2");
1281 assert_ne!(h1, h2);
1282 }
1283
1284 #[test]
1285 fn hash_sql_empty() {
1286 let _h = hash_sql(""); }
1288
1289 #[test]
1290 fn hash_sql_whitespace_only() {
1291 let h = hash_sql(" ");
1292 assert_ne!(h, hash_sql(""));
1293 }
1294
1295 #[test]
1296 fn hash_sql_very_long() {
1297 let long_sql = "SELECT ".to_string() + &"x".repeat(10_000);
1298 let h = hash_sql(&long_sql);
1299 assert_eq!(h, hash_sql(&long_sql));
1300 }
1301
1302 #[test]
1303 fn hash_sql_unicode() {
1304 let h = hash_sql("SELECT '\u{1F600}'");
1305 assert_ne!(h, hash_sql("SELECT 'x'"));
1306 }
1307
1308 #[test]
1313 fn notification_struct_fields() {
1314 let n = Notification {
1315 pid: 42,
1316 channel: "test_chan".to_owned(),
1317 payload: "hello".to_owned(),
1318 };
1319 assert_eq!(n.pid, 42);
1320 assert_eq!(n.channel, "test_chan");
1321 assert_eq!(n.payload, "hello");
1322 }
1323
1324 #[test]
1325 fn notification_clone() {
1326 let n = Notification {
1327 pid: 1,
1328 channel: "c".to_owned(),
1329 payload: "p".to_owned(),
1330 };
1331 let n2 = n.clone();
1332 assert_eq!(n2.pid, 1);
1333 assert_eq!(n2.channel, "c");
1334 }
1335
1336 #[test]
1337 fn notification_debug() {
1338 let n = Notification {
1339 pid: 1,
1340 channel: "c".to_owned(),
1341 payload: "p".to_owned(),
1342 };
1343 let dbg = format!("{n:?}");
1344 assert!(dbg.contains("Notification"));
1345 }
1346
1347 #[test]
1352 fn query_result_empty() {
1353 let result = QueryResult {
1354 all_col_offsets: vec![],
1355 num_cols: 0,
1356 columns: Arc::from(Vec::new()),
1357 affected_rows: 0,
1358 data_buf: None,
1359 };
1360 assert!(result.is_empty());
1361 assert_eq!(result.len(), 0);
1362 }
1363
1364 #[test]
1365 fn query_result_from_parts() {
1366 let result = QueryResult::from_parts(vec![(0, 4), (0, -1)], 2, Arc::from(Vec::new()), 5);
1367 assert_eq!(result.len(), 1);
1368 assert_eq!(result.num_cols, 2);
1369 assert_eq!(result.affected_rows, 5);
1370 }
1371
1372 #[test]
1373 fn query_result_affected_rows() {
1374 let result = QueryResult {
1375 all_col_offsets: vec![],
1376 num_cols: 0,
1377 columns: Arc::from(Vec::new()),
1378 affected_rows: 42,
1379 data_buf: None,
1380 };
1381 assert_eq!(result.affected_rows, 42);
1382 assert!(result.is_empty());
1383 }
1384
1385 fn make_data_row(columns: &[Option<&[u8]>]) -> Vec<u8> {
1392 let mut buf = Vec::new();
1393 buf.extend_from_slice(&(columns.len() as i16).to_be_bytes());
1394 for col in columns {
1395 match col {
1396 Some(data) => {
1397 buf.extend_from_slice(&(data.len() as i32).to_be_bytes());
1398 buf.extend_from_slice(data);
1399 }
1400 None => {
1401 buf.extend_from_slice(&(-1i32).to_be_bytes());
1402 }
1403 }
1404 }
1405 buf
1406 }
1407
1408 #[test]
1409 fn pg_data_row_get_i32() {
1410 let data = make_data_row(&[Some(&42i32.to_be_bytes())]);
1411 let row = PgDataRow::new(&data).unwrap();
1412 assert_eq!(row.get_i32(0), Some(42));
1413 assert_eq!(row.column_count(), 1);
1414 }
1415
1416 #[test]
1417 fn pg_data_row_get_i64() {
1418 let data = make_data_row(&[Some(&12345i64.to_be_bytes())]);
1419 let row = PgDataRow::new(&data).unwrap();
1420 assert_eq!(row.get_i64(0), Some(12345));
1421 }
1422
1423 #[test]
1424 fn pg_data_row_get_str() {
1425 let data = make_data_row(&[Some(b"hello")]);
1426 let row = PgDataRow::new(&data).unwrap();
1427 assert_eq!(row.get_str(0), Some("hello"));
1428 }
1429
1430 #[test]
1431 fn pg_data_row_get_bytes() {
1432 let data = make_data_row(&[Some(&[0xDE, 0xAD, 0xBE, 0xEF])]);
1433 let row = PgDataRow::new(&data).unwrap();
1434 assert_eq!(row.get_bytes(0), Some(&[0xDE, 0xAD, 0xBE, 0xEF][..]));
1435 }
1436
1437 #[test]
1438 fn pg_data_row_get_bool() {
1439 let data = make_data_row(&[Some(&[1u8])]);
1440 let row = PgDataRow::new(&data).unwrap();
1441 assert_eq!(row.get_bool(0), Some(true));
1442
1443 let data = make_data_row(&[Some(&[0u8])]);
1444 let row = PgDataRow::new(&data).unwrap();
1445 assert_eq!(row.get_bool(0), Some(false));
1446 }
1447
1448 #[test]
1449 fn pg_data_row_get_f64() {
1450 let data = make_data_row(&[Some(&3.14f64.to_be_bytes())]);
1451 let row = PgDataRow::new(&data).unwrap();
1452 assert!((row.get_f64(0).unwrap() - 3.14).abs() < 1e-10);
1453 }
1454
1455 #[test]
1456 fn pg_data_row_null_column() {
1457 let data = make_data_row(&[None]);
1458 let row = PgDataRow::new(&data).unwrap();
1459 assert!(row.is_null(0));
1460 assert_eq!(row.get_i32(0), None);
1461 assert_eq!(row.get_str(0), None);
1462 }
1463
1464 #[test]
1465 fn pg_data_row_multiple_columns() {
1466 let data = make_data_row(&[
1467 Some(&42i32.to_be_bytes()),
1468 Some(b"alice"),
1469 Some(b"alice@example.com"),
1470 Some(&[1u8]),
1471 Some(&3.14f64.to_be_bytes()),
1472 ]);
1473 let row = PgDataRow::new(&data).unwrap();
1474 assert_eq!(row.column_count(), 5);
1475 assert_eq!(row.get_i32(0), Some(42));
1476 assert_eq!(row.get_str(1), Some("alice"));
1477 assert_eq!(row.get_str(2), Some("alice@example.com"));
1478 assert_eq!(row.get_bool(3), Some(true));
1479 assert!((row.get_f64(4).unwrap() - 3.14).abs() < 1e-10);
1480 }
1481
1482 #[test]
1483 fn pg_data_row_mixed_null() {
1484 let data = make_data_row(&[Some(&42i32.to_be_bytes()), None, Some(b"text")]);
1485 let row = PgDataRow::new(&data).unwrap();
1486 assert_eq!(row.get_i32(0), Some(42));
1487 assert!(row.is_null(1));
1488 assert_eq!(row.get_str(1), None);
1489 assert_eq!(row.get_str(2), Some("text"));
1490 }
1491
1492 #[test]
1493 fn pg_data_row_empty() {
1494 let data = make_data_row(&[]);
1495 let row = PgDataRow::new(&data).unwrap();
1496 assert_eq!(row.column_count(), 0);
1497 }
1498
1499 #[test]
1500 fn pg_data_row_too_short() {
1501 let data = vec![0u8]; assert!(PgDataRow::new(&data).is_err());
1503 }
1504
1505 #[test]
1506 fn pg_data_row_truncated() {
1507 let mut data = Vec::new();
1509 data.extend_from_slice(&2i16.to_be_bytes());
1510 data.extend_from_slice(&4i32.to_be_bytes());
1511 data.extend_from_slice(&42i32.to_be_bytes());
1512 assert!(PgDataRow::new(&data).is_err());
1514 }
1515
1516 #[test]
1517 fn pg_data_row_get_i16() {
1518 let data = make_data_row(&[Some(&7i16.to_be_bytes())]);
1519 let row = PgDataRow::new(&data).unwrap();
1520 assert_eq!(row.get_i16(0), Some(7));
1521 }
1522
1523 #[test]
1524 fn pg_data_row_get_f32() {
1525 let data = make_data_row(&[Some(&2.5f32.to_be_bytes())]);
1526 let row = PgDataRow::new(&data).unwrap();
1527 assert!((row.get_f32(0).unwrap() - 2.5).abs() < 1e-6);
1528 }
1529
1530 #[test]
1531 fn pg_data_row_get_raw_null() {
1532 let data = make_data_row(&[None]);
1533 let row = PgDataRow::new(&data).unwrap();
1534 assert_eq!(row.get_raw(0), None);
1535 }
1536
1537 #[test]
1538 fn pg_data_row_get_raw_data() {
1539 let data = make_data_row(&[Some(&[1, 2, 3])]);
1540 let row = PgDataRow::new(&data).unwrap();
1541 assert_eq!(row.get_raw(0), Some(&[1u8, 2, 3][..]));
1542 }
1543
1544 #[test]
1545 fn pg_data_row_stack_alloc_16_columns() {
1546 let cols: Vec<Option<&[u8]>> = (0..16).map(|_| Some(&[0u8][..])).collect();
1548 let data = make_data_row(&cols);
1549 let row = PgDataRow::new(&data).unwrap();
1550 assert_eq!(row.column_count(), 16);
1551 for i in 0..16 {
1553 assert_eq!(row.get_raw(i), Some(&[0u8][..]));
1554 }
1555 }
1556
1557 #[test]
1562 fn inline_sequential_decode_five_columns() {
1563 let data = make_data_row(&[
1564 Some(&42i32.to_be_bytes()),
1565 Some(b"alice"),
1566 Some(b"alice@example.com"),
1567 Some(&[1u8]),
1568 Some(&3.14f64.to_be_bytes()),
1569 ]);
1570
1571 let mut pos: usize = 2; let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1576 pos += 4;
1577 assert_eq!(len, 4);
1578 let id = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1579 pos += len as usize;
1580 assert_eq!(id, 42);
1581
1582 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1584 pos += 4;
1585 assert_eq!(len, 5);
1586 let name = std::str::from_utf8(&data[pos..pos + len as usize]).unwrap();
1587 pos += len as usize;
1588 assert_eq!(name, "alice");
1589
1590 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1592 pos += 4;
1593 let email = std::str::from_utf8(&data[pos..pos + len as usize]).unwrap();
1594 pos += len as usize;
1595 assert_eq!(email, "alice@example.com");
1596
1597 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1599 pos += 4;
1600 assert_eq!(len, 1);
1601 let active = data[pos] != 0;
1602 pos += len as usize;
1603 assert!(active);
1604
1605 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1607 pos += 4;
1608 assert_eq!(len, 8);
1609 let score = f64::from_be_bytes([
1610 data[pos],
1611 data[pos + 1],
1612 data[pos + 2],
1613 data[pos + 3],
1614 data[pos + 4],
1615 data[pos + 5],
1616 data[pos + 6],
1617 data[pos + 7],
1618 ]);
1619 pos += len as usize;
1620 assert!((score - 3.14).abs() < 1e-10);
1621 assert_eq!(pos, data.len());
1622 }
1623
1624 #[test]
1626 fn inline_sequential_decode_with_nulls() {
1627 let data = make_data_row(&[
1628 Some(&42i32.to_be_bytes()),
1629 None, Some(b"text"),
1631 ]);
1632
1633 let mut pos: usize = 2;
1634
1635 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1637 pos += 4;
1638 let id = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1639 pos += len as usize;
1640 assert_eq!(id, 42);
1641
1642 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1644 pos += 4;
1645 let name: Option<&str> = if len < 0 {
1646 None
1647 } else {
1648 let s = std::str::from_utf8(&data[pos..pos + len as usize]).unwrap();
1649 pos += len as usize;
1650 Some(s)
1651 };
1652 assert!(name.is_none());
1653
1654 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1656 pos += 4;
1657 let txt = std::str::from_utf8(&data[pos..pos + len as usize]).unwrap();
1658 pos += len as usize;
1659 assert_eq!(txt, "text");
1660 assert_eq!(pos, data.len());
1661 }
1662
1663 #[test]
1665 fn inline_sequential_decode_all_scalar_types() {
1666 let data = make_data_row(&[
1667 Some(&[1u8]), Some(&7i16.to_be_bytes()), Some(&42i32.to_be_bytes()), Some(&12345i64.to_be_bytes()), Some(&2.5f32.to_be_bytes()), Some(&3.14f64.to_be_bytes()), ]);
1674
1675 let mut pos: usize = 2;
1676
1677 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1679 pos += 4;
1680 let v_bool = data[pos] != 0;
1681 pos += len as usize;
1682 assert!(v_bool);
1683
1684 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1686 pos += 4;
1687 let v_i16 = i16::from_be_bytes([data[pos], data[pos + 1]]);
1688 pos += len as usize;
1689 assert_eq!(v_i16, 7);
1690
1691 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1693 pos += 4;
1694 let v_i32 = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1695 pos += len as usize;
1696 assert_eq!(v_i32, 42);
1697
1698 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1700 pos += 4;
1701 let v_i64 = i64::from_be_bytes([
1702 data[pos],
1703 data[pos + 1],
1704 data[pos + 2],
1705 data[pos + 3],
1706 data[pos + 4],
1707 data[pos + 5],
1708 data[pos + 6],
1709 data[pos + 7],
1710 ]);
1711 pos += len as usize;
1712 assert_eq!(v_i64, 12345);
1713
1714 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1716 pos += 4;
1717 let v_f32 = f32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1718 pos += len as usize;
1719 assert!((v_f32 - 2.5).abs() < 1e-6);
1720
1721 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1723 pos += 4;
1724 let v_f64 = f64::from_be_bytes([
1725 data[pos],
1726 data[pos + 1],
1727 data[pos + 2],
1728 data[pos + 3],
1729 data[pos + 4],
1730 data[pos + 5],
1731 data[pos + 6],
1732 data[pos + 7],
1733 ]);
1734 pos += len as usize;
1735 assert!((v_f64 - 3.14).abs() < 1e-10);
1736 assert_eq!(pos, data.len());
1737 }
1738
1739 #[test]
1741 fn pg_data_row_new_is_public() {
1742 let data = make_data_row(&[Some(&42i32.to_be_bytes())]);
1743 let row = PgDataRow::new(&data).unwrap();
1745 assert_eq!(row.get_i32(0), Some(42));
1746 }
1747
1748 #[test]
1750 fn inline_decode_matches_pgdatarow() {
1751 let data = make_data_row(&[
1752 Some(&99i32.to_be_bytes()),
1753 Some(b"hello world"),
1754 None,
1755 Some(&[0u8]),
1756 Some(&1.23f64.to_be_bytes()),
1757 ]);
1758
1759 let row = PgDataRow::new(&data).unwrap();
1761 let dr_i32 = row.get_i32(0);
1762 let dr_str = row.get_str(1);
1763 let dr_null = row.get_str(2);
1764 let dr_bool = row.get_bool(3);
1765 let dr_f64 = row.get_f64(4);
1766
1767 let mut pos: usize = 2;
1769
1770 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1771 pos += 4;
1772 let in_i32 = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1773 pos += len as usize;
1774
1775 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1776 pos += 4;
1777 let in_str = std::str::from_utf8(&data[pos..pos + len as usize]).unwrap();
1778 pos += len as usize;
1779
1780 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1781 pos += 4;
1782 let in_null: Option<&str> = if len < 0 { None } else { unreachable!() };
1783
1784 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1785 pos += 4;
1786 let in_bool = data[pos] != 0;
1787 pos += len as usize;
1788
1789 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1790 pos += 4;
1791 let in_f64 = f64::from_be_bytes([
1792 data[pos],
1793 data[pos + 1],
1794 data[pos + 2],
1795 data[pos + 3],
1796 data[pos + 4],
1797 data[pos + 5],
1798 data[pos + 6],
1799 data[pos + 7],
1800 ]);
1801 pos += len as usize;
1802
1803 assert_eq!(dr_i32, Some(in_i32));
1805 assert_eq!(dr_str, Some(in_str));
1806 assert_eq!(dr_null, in_null);
1807 assert_eq!(dr_bool, Some(in_bool));
1808 assert!((dr_f64.unwrap() - in_f64).abs() < 1e-15);
1809 assert_eq!(pos, data.len());
1810 }
1811
1812 #[test]
1817 fn pg_data_row_all_null_columns() {
1818 let data = make_data_row(&[None, None, None, None, None]);
1819 let row = PgDataRow::new(&data).unwrap();
1820 assert_eq!(row.column_count(), 5);
1821 for i in 0..5 {
1822 assert!(row.is_null(i), "column {i} should be null");
1823 assert_eq!(row.get_raw(i), None);
1824 assert_eq!(row.get_i32(i), None);
1825 assert_eq!(row.get_i64(i), None);
1826 assert_eq!(row.get_str(i), None);
1827 assert_eq!(row.get_bool(i), None);
1828 assert_eq!(row.get_f64(i), None);
1829 }
1830 }
1831
1832 #[test]
1833 fn pg_data_row_very_long_text() {
1834 let long_text = "x".repeat(2048);
1835 let data = make_data_row(&[Some(long_text.as_bytes())]);
1836 let row = PgDataRow::new(&data).unwrap();
1837 assert_eq!(row.get_str(0), Some(long_text.as_str()));
1838 }
1839
1840 #[test]
1841 fn pg_data_row_empty_text() {
1842 let data = make_data_row(&[Some(b"")]);
1843 let row = PgDataRow::new(&data).unwrap();
1844 assert!(!row.is_null(0));
1845 assert_eq!(row.get_str(0), Some(""));
1846 assert_eq!(row.get_bytes(0), Some(&[][..]));
1847 }
1848
1849 #[test]
1850 fn pg_data_row_20_columns_exceeds_inline() {
1851 let col_data: Vec<[u8; 4]> = (0..20).map(|i: i32| i.to_be_bytes()).collect();
1852 let cols: Vec<Option<&[u8]>> = col_data.iter().map(|b| Some(b.as_slice())).collect();
1853 let data = make_data_row(&cols);
1854 let row = PgDataRow::new(&data).unwrap();
1855 assert_eq!(row.column_count(), 20);
1856 for i in 0..20 {
1857 assert_eq!(row.get_i32(i), Some(i as i32));
1858 }
1859 }
1860
1861 #[test]
1862 fn pg_data_row_is_null_each_position() {
1863 let data = make_data_row(&[Some(&1i32.to_be_bytes()), None, Some(&3i32.to_be_bytes())]);
1865 let row = PgDataRow::new(&data).unwrap();
1866 assert!(!row.is_null(0));
1867 assert!(row.is_null(1));
1868 assert!(!row.is_null(2));
1869 }
1870
1871 #[test]
1872 fn pg_data_row_negative_column_count() {
1873 let data = (-1i16).to_be_bytes();
1874 assert!(PgDataRow::new(&data).is_err());
1875 }
1876
1877 #[test]
1878 fn pg_data_row_get_str_invalid_utf8() {
1879 let invalid_utf8 = &[0xFF, 0xFE, 0x80];
1880 let data = make_data_row(&[Some(invalid_utf8)]);
1881 let row = PgDataRow::new(&data).unwrap();
1882 assert_eq!(row.get_str(0), None);
1884 assert_eq!(row.get_bytes(0), Some(&[0xFF, 0xFE, 0x80][..]));
1885 }
1886
1887 #[test]
1888 fn pg_data_row_get_i32_wrong_length() {
1889 let data = make_data_row(&[Some(&7i16.to_be_bytes())]);
1891 let row = PgDataRow::new(&data).unwrap();
1892 assert_eq!(row.get_i32(0), None); assert_eq!(row.get_i16(0), Some(7)); }
1895
1896 #[test]
1897 fn pg_data_row_get_i64_wrong_length() {
1898 let data = make_data_row(&[Some(&42i32.to_be_bytes())]);
1900 let row = PgDataRow::new(&data).unwrap();
1901 assert_eq!(row.get_i64(0), None);
1902 }
1903
1904 #[test]
1905 fn pg_data_row_get_f64_wrong_length() {
1906 let data = make_data_row(&[Some(&2.5f32.to_be_bytes())]);
1907 let row = PgDataRow::new(&data).unwrap();
1908 assert_eq!(row.get_f64(0), None); }
1910
1911 #[test]
1912 fn pg_data_row_get_f32_wrong_length() {
1913 let data = make_data_row(&[Some(&3.14f64.to_be_bytes())]);
1914 let row = PgDataRow::new(&data).unwrap();
1915 assert_eq!(row.get_f32(0), None); }
1917
1918 #[test]
1919 fn pg_data_row_get_bool_wrong_length() {
1920 let data = make_data_row(&[Some(&42i32.to_be_bytes())]);
1922 let row = PgDataRow::new(&data).unwrap();
1923 assert_eq!(row.get_bool(0), None);
1924 }
1925
1926 #[test]
1927 fn pg_data_row_unicode_text() {
1928 let texts = [
1929 "\u{1F600}\u{1F4A9}\u{1F680}", "\u{4e16}\u{754c}", "\u{0645}\u{0631}\u{062D}", "\u{1F468}\u{200D}\u{1F469}", ];
1934 for text in &texts {
1935 let data = make_data_row(&[Some(text.as_bytes())]);
1936 let row = PgDataRow::new(&data).unwrap();
1937 assert_eq!(row.get_str(0), Some(*text));
1938 }
1939 }
1940
1941 #[test]
1942 fn pg_data_row_i32_boundary_values() {
1943 for &val in &[i32::MIN, -1, 0, 1, i32::MAX] {
1944 let data = make_data_row(&[Some(&val.to_be_bytes())]);
1945 let row = PgDataRow::new(&data).unwrap();
1946 assert_eq!(row.get_i32(0), Some(val), "failed for {val}");
1947 }
1948 }
1949
1950 #[test]
1951 fn pg_data_row_i64_boundary_values() {
1952 for &val in &[i64::MIN, -1, 0, 1, i64::MAX] {
1953 let data = make_data_row(&[Some(&val.to_be_bytes())]);
1954 let row = PgDataRow::new(&data).unwrap();
1955 assert_eq!(row.get_i64(0), Some(val), "failed for {val}");
1956 }
1957 }
1958
1959 #[test]
1960 fn pg_data_row_f64_special_values() {
1961 let data = make_data_row(&[Some(&f64::INFINITY.to_be_bytes())]);
1962 let row = PgDataRow::new(&data).unwrap();
1963 assert_eq!(row.get_f64(0), Some(f64::INFINITY));
1964
1965 let data = make_data_row(&[Some(&f64::NEG_INFINITY.to_be_bytes())]);
1966 let row = PgDataRow::new(&data).unwrap();
1967 assert_eq!(row.get_f64(0), Some(f64::NEG_INFINITY));
1968
1969 let data = make_data_row(&[Some(&f64::NAN.to_be_bytes())]);
1970 let row = PgDataRow::new(&data).unwrap();
1971 assert!(row.get_f64(0).unwrap().is_nan());
1972 }
1973
1974 #[test]
1975 fn pg_data_row_f32_special_values() {
1976 let data = make_data_row(&[Some(&f32::INFINITY.to_be_bytes())]);
1977 let row = PgDataRow::new(&data).unwrap();
1978 assert_eq!(row.get_f32(0), Some(f32::INFINITY));
1979
1980 let data = make_data_row(&[Some(&f32::NAN.to_be_bytes())]);
1981 let row = PgDataRow::new(&data).unwrap();
1982 assert!(row.get_f32(0).unwrap().is_nan());
1983 }
1984
1985 #[test]
1986 fn pg_data_row_i16_boundary_values() {
1987 for &val in &[i16::MIN, -1, 0, 1, i16::MAX] {
1988 let data = make_data_row(&[Some(&val.to_be_bytes())]);
1989 let row = PgDataRow::new(&data).unwrap();
1990 assert_eq!(row.get_i16(0), Some(val));
1991 }
1992 }
1993}