1use std::sync::Arc;
7
8use rapidhash::quality::RapidHasher;
9
10use crate::arena::Arena;
11use crate::DriverError;
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 table_oid: u32,
259 pub type_size: i16,
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 #[inline]
454 pub fn get_raw(&self, idx: usize) -> Option<&'a [u8]> {
455 let (offset, len) = self.col_offsets[idx];
456 if len < 0 {
457 None
458 } else if let Some(buf) = self.data {
459 Some(&buf[offset..offset + len as usize])
460 } else {
461 Some(self.arena.get(offset, len as usize))
462 }
463 }
464
465 #[inline]
467 pub fn is_null(&self, idx: usize) -> bool {
468 self.col_offsets[idx].1 < 0
469 }
470
471 #[inline]
473 pub fn column_count(&self) -> usize {
474 self.col_offsets.len()
475 }
476
477 #[inline]
479 pub fn get_bool(&self, idx: usize) -> Option<bool> {
480 self.get_raw(idx)
481 .and_then(|data| crate::codec::decode_bool(data).ok())
482 }
483
484 #[inline]
486 pub fn get_i16(&self, idx: usize) -> Option<i16> {
487 self.get_raw(idx)
488 .and_then(|data| crate::codec::decode_i16(data).ok())
489 }
490
491 #[inline]
493 pub fn get_i32(&self, idx: usize) -> Option<i32> {
494 self.get_raw(idx)
495 .and_then(|data| crate::codec::decode_i32(data).ok())
496 }
497
498 #[inline]
500 pub fn get_i64(&self, idx: usize) -> Option<i64> {
501 self.get_raw(idx)
502 .and_then(|data| crate::codec::decode_i64(data).ok())
503 }
504
505 #[inline]
507 pub fn get_f32(&self, idx: usize) -> Option<f32> {
508 self.get_raw(idx)
509 .and_then(|data| crate::codec::decode_f32(data).ok())
510 }
511
512 #[inline]
514 pub fn get_f64(&self, idx: usize) -> Option<f64> {
515 self.get_raw(idx)
516 .and_then(|data| crate::codec::decode_f64(data).ok())
517 }
518
519 #[inline]
521 pub fn get_str(&self, idx: usize) -> Option<&'a str> {
522 self.get_raw(idx)
523 .and_then(|data| crate::codec::decode_str(data).ok())
524 }
525
526 #[inline]
528 pub fn get_bytes(&self, idx: usize) -> Option<&'a [u8]> {
529 self.get_raw(idx)
530 }
531
532 #[inline]
534 pub fn column_name(&self, idx: usize) -> &str {
535 &self.columns[idx].name
536 }
537
538 #[inline]
540 pub fn column_type_oid(&self, idx: usize) -> u32 {
541 self.columns[idx].type_oid
542 }
543}
544
545pub struct PgDataRow<'a> {
558 data: &'a [u8],
559 offsets: smallvec::SmallVec<[(usize, i32); 16]>,
562}
563
564impl<'a> PgDataRow<'a> {
565 pub fn new(data: &'a [u8]) -> Result<Self, DriverError> {
570 if data.len() < 2 {
571 return Err(DriverError::Protocol("DataRow too short".into()));
572 }
573 let num_cols = i16::from_be_bytes([data[0], data[1]]);
574 if num_cols < 0 {
575 return Err(DriverError::Protocol(
576 "DataRow: negative column count".into(),
577 ));
578 }
579 let num_cols = num_cols as usize;
580 let mut offsets = smallvec::SmallVec::<[(usize, i32); 16]>::with_capacity(num_cols);
581 let mut pos = 2usize;
582 for _ in 0..num_cols {
583 if pos + 4 > data.len() {
584 return Err(DriverError::Protocol("DataRow truncated".into()));
585 }
586 let col_len =
587 i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
588 pos += 4;
589 offsets.push((pos, col_len));
590 if col_len > 0 {
591 pos += col_len as usize;
592 }
593 }
594 Ok(Self { data, offsets })
595 }
596
597 #[inline]
599 pub fn get_raw(&self, idx: usize) -> Option<&'a [u8]> {
600 let (offset, len) = self.offsets[idx];
601 if len < 0 {
602 None
603 } else {
604 Some(&self.data[offset..offset + len as usize])
605 }
606 }
607
608 #[inline]
610 pub fn is_null(&self, idx: usize) -> bool {
611 self.offsets[idx].1 < 0
612 }
613
614 #[inline]
616 pub fn column_count(&self) -> usize {
617 self.offsets.len()
618 }
619
620 #[inline]
622 pub fn get_bool(&self, idx: usize) -> Option<bool> {
623 self.get_raw(idx)
624 .and_then(|data| crate::codec::decode_bool(data).ok())
625 }
626
627 #[inline]
629 pub fn get_i16(&self, idx: usize) -> Option<i16> {
630 self.get_raw(idx)
631 .and_then(|data| crate::codec::decode_i16(data).ok())
632 }
633
634 #[inline]
636 pub fn get_i32(&self, idx: usize) -> Option<i32> {
637 self.get_raw(idx)
638 .and_then(|data| crate::codec::decode_i32(data).ok())
639 }
640
641 #[inline]
643 pub fn get_i64(&self, idx: usize) -> Option<i64> {
644 self.get_raw(idx)
645 .and_then(|data| crate::codec::decode_i64(data).ok())
646 }
647
648 #[inline]
650 pub fn get_f32(&self, idx: usize) -> Option<f32> {
651 self.get_raw(idx)
652 .and_then(|data| crate::codec::decode_f32(data).ok())
653 }
654
655 #[inline]
657 pub fn get_f64(&self, idx: usize) -> Option<f64> {
658 self.get_raw(idx)
659 .and_then(|data| crate::codec::decode_f64(data).ok())
660 }
661
662 #[inline]
664 pub fn get_str(&self, idx: usize) -> Option<&'a str> {
665 self.get_raw(idx)
666 .and_then(|data| crate::codec::decode_str(data).ok())
667 }
668
669 #[inline]
671 pub fn get_bytes(&self, idx: usize) -> Option<&'a [u8]> {
672 self.get_raw(idx)
673 }
674}
675
676pub fn hash_sql(sql: &str) -> u64 {
684 use std::hash::{Hash, Hasher};
685 let mut hasher = RapidHasher::default();
686 sql.hash(&mut hasher);
687 hasher.finish()
688}
689
690#[cfg(test)]
695#[allow(clippy::approx_constant)]
696mod tests {
697 use super::*;
698
699 #[test]
704 fn config_parse_full_url() {
705 let cfg = Config::from_url("postgres://user:pass@localhost:5432/mydb").unwrap();
706 assert_eq!(cfg.user, "user");
707 assert_eq!(cfg.password, "pass");
708 assert_eq!(cfg.host, "localhost");
709 assert_eq!(cfg.port, 5432);
710 assert_eq!(cfg.database, "mydb");
711 }
712
713 #[test]
714 fn config_parse_default_port() {
715 let cfg = Config::from_url("postgres://user:pass@localhost/mydb").unwrap();
716 assert_eq!(cfg.port, 5432);
717 }
718
719 #[test]
720 fn config_parse_no_password() {
721 let cfg = Config::from_url("postgres://user@localhost/mydb").unwrap();
722 assert_eq!(cfg.user, "user");
723 assert_eq!(cfg.password, "");
724 }
725
726 #[test]
727 fn config_parse_empty_database() {
728 let cfg = Config::from_url("postgres://user:pass@localhost").unwrap();
729 assert_eq!(cfg.database, "user");
731 }
732
733 #[test]
734 fn config_parse_sslmode() {
735 let cfg = Config::from_url("postgres://user:pass@localhost/db?sslmode=require").unwrap();
736 assert_eq!(cfg.ssl, SslMode::Require);
737 }
738
739 #[test]
740 fn config_parse_percent_encoding() {
741 let cfg = Config::from_url("postgres://user%40domain:p%40ss@localhost/db").unwrap();
742 assert_eq!(cfg.user, "user@domain");
743 assert_eq!(cfg.password, "p@ss");
744 }
745
746 #[test]
747 fn config_rejects_bad_scheme() {
748 let result = Config::from_url("mysql://user:pass@localhost/db");
749 assert!(result.is_err());
750 }
751
752 #[test]
754 fn config_rejects_unknown_sslmode() {
755 let result = Config::from_url("postgres://user:pass@localhost/db?sslmode=requre");
756 assert!(result.is_err(), "typo 'requre' should be rejected");
757 let result = Config::from_url("postgres://user:pass@localhost/db?sslmode=REQUIRE");
758 assert!(result.is_err(), "uppercase should be rejected");
759 let result = Config::from_url("postgres://user:pass@localhost/db?sslmode=bogus");
760 assert!(result.is_err(), "bogus value should be rejected");
761 }
762
763 #[test]
765 fn config_accepts_valid_sslmodes() {
766 let cfg = Config::from_url("postgres://user:pass@localhost/db?sslmode=disable").unwrap();
767 assert_eq!(cfg.ssl, SslMode::Disable);
768 let cfg = Config::from_url("postgres://user:pass@localhost/db?sslmode=prefer").unwrap();
769 assert_eq!(cfg.ssl, SslMode::Prefer);
770 let cfg = Config::from_url("postgres://user:pass@localhost/db?sslmode=require").unwrap();
771 assert_eq!(cfg.ssl, SslMode::Require);
772 }
773
774 #[test]
776 fn config_parse_postgresql_scheme() {
777 let cfg = Config::from_url("postgresql://user:pass@localhost:5432/mydb").unwrap();
778 assert_eq!(cfg.user, "user");
779 assert_eq!(cfg.password, "pass");
780 assert_eq!(cfg.host, "localhost");
781 assert_eq!(cfg.port, 5432);
782 assert_eq!(cfg.database, "mydb");
783 }
784
785 #[test]
787 fn config_parse_no_password_standalone() {
788 let cfg = Config::from_url("postgres://admin@db.example.com/myapp").unwrap();
789 assert_eq!(cfg.user, "admin");
790 assert_eq!(cfg.password, "");
791 assert_eq!(cfg.host, "db.example.com");
792 assert_eq!(cfg.database, "myapp");
793 }
794
795 #[test]
797 fn config_empty_database_falls_back_to_user() {
798 let cfg = Config::from_url("postgres://testuser:pass@localhost").unwrap();
799 assert_eq!(cfg.database, "testuser");
800 }
801
802 #[test]
804 fn config_unknown_sslmode_error() {
805 let result = Config::from_url("postgres://u:p@h/d?sslmode=verify-full");
806 assert!(result.is_err());
807 let err = result.unwrap_err().to_string();
808 assert!(
809 err.contains("unknown sslmode"),
810 "should describe unknown sslmode: {err}"
811 );
812 }
813
814 #[test]
816 fn config_multiple_query_params() {
817 let cfg = Config::from_url(
818 "postgres://user:pass@localhost/db?sslmode=disable&statement_timeout=60",
819 )
820 .unwrap();
821 assert_eq!(cfg.ssl, SslMode::Disable);
822 assert_eq!(cfg.statement_timeout_secs, 60);
823 }
824
825 #[test]
827 fn config_validate_empty_host() {
828 let cfg = Config {
829 host: String::new(),
830 port: 5432,
831 user: "user".into(),
832 password: "pass".into(),
833 database: "db".into(),
834 ssl: SslMode::Disable,
835 statement_timeout_secs: 30,
836 };
837 assert!(cfg.validate().is_err());
838 }
839
840 #[test]
842 fn config_validate_empty_user() {
843 let cfg = Config {
844 host: "localhost".into(),
845 port: 5432,
846 user: String::new(),
847 password: "pass".into(),
848 database: "db".into(),
849 ssl: SslMode::Disable,
850 statement_timeout_secs: 30,
851 };
852 assert!(cfg.validate().is_err());
853 }
854
855 #[test]
857 fn config_validate_empty_database() {
858 let cfg = Config {
859 host: "localhost".into(),
860 port: 5432,
861 user: "user".into(),
862 password: "pass".into(),
863 database: String::new(),
864 ssl: SslMode::Disable,
865 statement_timeout_secs: 30,
866 };
867 assert!(cfg.validate().is_err());
868 }
869
870 #[test]
872 fn config_missing_at_sign() {
873 let result = Config::from_url("postgres://userpasslocalhost/db");
874 assert!(result.is_err());
875 }
876
877 #[test]
879 fn config_custom_port() {
880 let cfg = Config::from_url("postgres://user:pass@localhost:5433/db").unwrap();
881 assert_eq!(cfg.port, 5433);
882 }
883
884 #[test]
886 fn config_invalid_port() {
887 let result = Config::from_url("postgres://user:pass@localhost:notaport/db");
888 assert!(result.is_err());
889 }
890
891 #[cfg(not(feature = "tls"))]
893 #[test]
894 fn config_sslmode_require_without_tls_feature() {
895 let cfg = Config::from_url("postgres://user:pass@localhost/db?sslmode=require").unwrap();
898 assert_eq!(cfg.ssl, SslMode::Require);
899 }
900
901 #[test]
902 fn config_statement_timeout_default() {
903 let cfg = Config::from_url("postgres://user:pass@localhost/db").unwrap();
904 assert_eq!(cfg.statement_timeout_secs, 30);
905 }
906
907 #[test]
908 fn config_statement_timeout_custom() {
909 let cfg =
910 Config::from_url("postgres://user:pass@localhost/db?statement_timeout=120").unwrap();
911 assert_eq!(cfg.statement_timeout_secs, 120);
912 }
913
914 #[test]
915 fn config_statement_timeout_zero() {
916 let cfg =
917 Config::from_url("postgres://user:pass@localhost/db?statement_timeout=0").unwrap();
918 assert_eq!(cfg.statement_timeout_secs, 0);
919 }
920
921 #[test]
922 fn config_statement_timeout_invalid_falls_back() {
923 let cfg =
924 Config::from_url("postgres://user:pass@localhost/db?statement_timeout=notanumber")
925 .unwrap();
926 assert_eq!(cfg.statement_timeout_secs, 30); }
928
929 #[test]
930 fn config_uds_path_format() {
931 let cfg = Config::from_url("postgres://user@localhost/db?host=/tmp").unwrap();
932 assert_eq!(cfg.uds_path(), "/tmp/.s.PGSQL.5432");
933 }
934
935 #[test]
936 fn config_uds_path_custom_port() {
937 let cfg = Config::from_url("postgres://user@localhost:5433/db?host=/tmp").unwrap();
938 assert_eq!(cfg.uds_path(), "/tmp/.s.PGSQL.5433");
939 }
940
941 #[test]
946 fn config_host_is_uds_absolute_path() {
947 let cfg = Config {
948 host: "/tmp".into(),
949 port: 5432,
950 user: "user".into(),
951 password: "".into(),
952 database: "db".into(),
953 ssl: SslMode::Disable,
954 statement_timeout_secs: 30,
955 };
956 assert!(cfg.host_is_uds());
957 assert_eq!(cfg.uds_path(), "/tmp/.s.PGSQL.5432");
958 }
959
960 #[test]
961 fn config_host_is_uds_var_run() {
962 let cfg = Config {
963 host: "/var/run/postgresql".into(),
964 port: 5433,
965 user: "user".into(),
966 password: "".into(),
967 database: "db".into(),
968 ssl: SslMode::Disable,
969 statement_timeout_secs: 30,
970 };
971 assert!(cfg.host_is_uds());
972 assert_eq!(cfg.uds_path(), "/var/run/postgresql/.s.PGSQL.5433");
973 }
974
975 #[test]
976 fn config_host_is_not_uds_for_hostname() {
977 let cfg = Config {
978 host: "localhost".into(),
979 port: 5432,
980 user: "user".into(),
981 password: "".into(),
982 database: "db".into(),
983 ssl: SslMode::Disable,
984 statement_timeout_secs: 30,
985 };
986 assert!(!cfg.host_is_uds());
987 }
988
989 #[test]
990 fn config_host_is_not_uds_for_ip() {
991 let cfg = Config {
992 host: "127.0.0.1".into(),
993 port: 5432,
994 user: "user".into(),
995 password: "".into(),
996 database: "db".into(),
997 ssl: SslMode::Disable,
998 statement_timeout_secs: 30,
999 };
1000 assert!(!cfg.host_is_uds());
1001 }
1002
1003 #[test]
1004 fn config_parse_uds_host_query_param() {
1005 let cfg = Config::from_url("postgres://user@localhost/mydb?host=/tmp").unwrap();
1006 assert_eq!(cfg.host, "/tmp");
1007 assert!(cfg.host_is_uds());
1008 assert_eq!(cfg.uds_path(), "/tmp/.s.PGSQL.5432");
1009 assert_eq!(cfg.database, "mydb");
1010 assert_eq!(cfg.user, "user");
1011 }
1012
1013 #[test]
1014 fn config_parse_uds_host_query_param_custom_port() {
1015 let cfg = Config::from_url("postgres://user@localhost:5433/mydb?host=/var/run/postgresql")
1016 .unwrap();
1017 assert_eq!(cfg.host, "/var/run/postgresql");
1018 assert_eq!(cfg.port, 5433);
1019 assert_eq!(cfg.uds_path(), "/var/run/postgresql/.s.PGSQL.5433");
1020 }
1021
1022 #[test]
1023 fn config_parse_uds_host_with_other_params() {
1024 let cfg = Config::from_url(
1025 "postgres://user@localhost/db?host=/tmp&sslmode=disable&statement_timeout=60",
1026 )
1027 .unwrap();
1028 assert_eq!(cfg.host, "/tmp");
1029 assert!(cfg.host_is_uds());
1030 assert_eq!(cfg.ssl, SslMode::Disable);
1031 assert_eq!(cfg.statement_timeout_secs, 60);
1032 }
1033
1034 #[test]
1035 fn config_parse_uds_host_percent_encoded() {
1036 let cfg = Config::from_url("postgres://user@localhost/db?host=%2Ftmp").unwrap();
1038 assert_eq!(cfg.host, "/tmp");
1039 assert!(cfg.host_is_uds());
1040 }
1041
1042 #[test]
1043 fn config_parse_tcp_host_not_overridden_without_param() {
1044 let cfg = Config::from_url("postgres://user@myserver/db").unwrap();
1046 assert_eq!(cfg.host, "myserver");
1047 assert!(!cfg.host_is_uds());
1048 }
1049
1050 #[test]
1051 fn config_parse_uds_host_overrides_url_hostname() {
1052 let cfg = Config::from_url("postgres://user@db.example.com/mydb?host=/var/run/postgresql")
1054 .unwrap();
1055 assert_eq!(cfg.host, "/var/run/postgresql");
1056 assert!(cfg.host_is_uds());
1057 }
1058
1059 #[test]
1060 fn config_parse_uds_empty_url_host() {
1061 let cfg = Config::from_url("postgres://user@/mydb?host=/tmp").unwrap();
1063 assert_eq!(cfg.host, "/tmp");
1064 assert!(cfg.host_is_uds());
1065 assert_eq!(cfg.database, "mydb");
1066 }
1067
1068 #[test]
1073 fn url_decode_works() {
1074 assert_eq!(url_decode("hello%20world").unwrap(), "hello world");
1075 assert_eq!(url_decode("no%20escape").unwrap(), "no escape");
1076 assert_eq!(url_decode("plain").unwrap(), "plain");
1077 assert_eq!(url_decode("a%40b").unwrap(), "a@b");
1078 }
1079
1080 #[test]
1081 fn url_decode_malformed_percent_trailing() {
1082 let result = url_decode("abc%2");
1084 assert!(result.is_err(), "truncated %2 should error");
1085 }
1086
1087 #[test]
1088 fn url_decode_malformed_percent_no_digits() {
1089 let result = url_decode("abc%");
1091 assert!(result.is_err(), "bare % at end should error");
1092 }
1093
1094 #[test]
1095 fn url_decode_invalid_hex_digit() {
1096 let result = url_decode("abc%GG");
1098 assert!(result.is_err(), "%GG should error");
1099 }
1100
1101 #[test]
1102 fn url_decode_invalid_hex_second_digit() {
1103 let result = url_decode("abc%2Z");
1105 assert!(result.is_err(), "%2Z should error");
1106 }
1107
1108 #[test]
1110 fn url_decode_invalid_utf8_percent() {
1111 let result = url_decode("%80%81");
1113 assert!(result.is_err(), "invalid UTF-8 bytes should error");
1114 }
1115
1116 #[test]
1118 fn url_decode_percent_everywhere() {
1119 assert_eq!(url_decode("%41%42%43").unwrap(), "ABC");
1120 assert_eq!(url_decode("%61").unwrap(), "a");
1121 assert_eq!(url_decode("x%2Fy%2Fz").unwrap(), "x/y/z");
1122 }
1123
1124 #[test]
1126 fn url_decode_bare_percent_middle() {
1127 assert!(url_decode("a%b").is_err(), "bare % in middle should error");
1128 }
1129
1130 #[test]
1132 fn url_decode_multibyte_utf8() {
1133 let result = url_decode("caf%C3%A9").unwrap();
1134 assert_eq!(result, "caf\u{00e9}"); }
1136
1137 #[test]
1139 fn url_decode_invalid_percent_zz() {
1140 let result = url_decode("abc%ZZ");
1141 assert!(result.is_err(), "%ZZ should error");
1142 }
1143
1144 #[test]
1146 fn url_decode_truncated_percent_trailing() {
1147 let result = url_decode("abc%");
1148 assert!(result.is_err(), "trailing % should error");
1149 }
1150
1151 #[test]
1153 fn url_decode_invalid_utf8() {
1154 let result = url_decode("%80");
1156 assert!(result.is_err(), "invalid UTF-8 should error");
1157 }
1158
1159 #[test]
1160 fn url_decode_empty_string() {
1161 assert_eq!(url_decode("").unwrap(), "");
1162 }
1163
1164 #[test]
1165 fn url_decode_no_encoding() {
1166 assert_eq!(url_decode("hello").unwrap(), "hello");
1167 }
1168
1169 #[test]
1170 fn url_decode_all_ascii_hex() {
1171 assert_eq!(url_decode("%2F").unwrap(), "/");
1173 assert_eq!(url_decode("%2f").unwrap(), "/");
1174 }
1175
1176 #[test]
1180 fn config_unicode_password() {
1181 let cfg =
1183 Config::from_url("postgres://user:%D0%BF%D0%B0%D1%80%D0%BE%D0%BB%D1%8C@localhost/db")
1184 .unwrap();
1185 assert_eq!(cfg.user, "user");
1186 assert_eq!(
1187 cfg.password,
1188 "\u{043F}\u{0430}\u{0440}\u{043E}\u{043B}\u{044C}"
1189 ); assert_eq!(cfg.host, "localhost");
1191 assert_eq!(cfg.database, "db");
1192 }
1193
1194 #[test]
1196 fn config_port_zero() {
1197 let cfg = Config::from_url("postgres://user:pass@localhost:0/db").unwrap();
1198 assert_eq!(cfg.port, 0);
1199 }
1200
1201 #[test]
1203 fn config_port_max() {
1204 let cfg = Config::from_url("postgres://user:pass@localhost:65535/db").unwrap();
1205 assert_eq!(cfg.port, 65535);
1206 }
1207
1208 #[test]
1210 fn config_port_overflow() {
1211 let result = Config::from_url("postgres://user:pass@localhost:65536/db");
1212 assert!(result.is_err(), "port 65536 exceeds u16 max");
1213 }
1214
1215 #[test]
1217 fn config_unknown_param_ignored() {
1218 let cfg = Config::from_url(
1219 "postgres://user:pass@localhost/db?application_name=myapp&connect_timeout=10",
1220 )
1221 .unwrap();
1222 assert_eq!(cfg.user, "user");
1224 assert_eq!(cfg.host, "localhost");
1225 assert_eq!(cfg.database, "db");
1226 assert_eq!(cfg.statement_timeout_secs, 30);
1228 assert_eq!(cfg.ssl, SslMode::Prefer);
1229 }
1230
1231 #[test]
1233 fn url_decode_double_percent_encoding() {
1234 assert_eq!(url_decode("%2525").unwrap(), "%25");
1236 }
1237
1238 #[test]
1240 fn config_explicit_empty_password() {
1241 let cfg = Config::from_url("postgres://user:@localhost/db").unwrap();
1242 assert_eq!(cfg.user, "user");
1243 assert_eq!(cfg.password, "");
1244 }
1245
1246 #[test]
1248 fn config_special_chars_in_user() {
1249 let cfg = Config::from_url("postgres://my%2Fuser:pass@localhost/my%2Fdb").unwrap();
1250 assert_eq!(cfg.user, "my/user");
1251 assert_eq!(cfg.database, "my/db");
1252 }
1253
1254 #[test]
1256 fn url_decode_plus_is_literal() {
1257 assert_eq!(url_decode("a+b").unwrap(), "a+b");
1258 }
1259
1260 #[test]
1262 fn config_minimal_valid_url() {
1263 let cfg = Config::from_url("postgres://user@localhost/db").unwrap();
1264 assert_eq!(cfg.user, "user");
1265 assert_eq!(cfg.password, "");
1266 assert_eq!(cfg.host, "localhost");
1267 assert_eq!(cfg.port, 5432);
1268 assert_eq!(cfg.database, "db");
1269 }
1270
1271 #[test]
1273 fn config_empty_param_segments() {
1274 let cfg =
1275 Config::from_url("postgres://user:pass@localhost/db?&&statement_timeout=60&&").unwrap();
1276 assert_eq!(cfg.statement_timeout_secs, 60);
1277 }
1278
1279 #[test]
1284 fn hash_sql_deterministic() {
1285 let h1 = hash_sql("SELECT 1");
1286 let h2 = hash_sql("SELECT 1");
1287 assert_eq!(h1, h2);
1288 }
1289
1290 #[test]
1291 fn hash_sql_different_queries() {
1292 let h1 = hash_sql("SELECT 1");
1293 let h2 = hash_sql("SELECT 2");
1294 assert_ne!(h1, h2);
1295 }
1296
1297 #[test]
1298 fn hash_sql_empty() {
1299 let _h = hash_sql(""); }
1301
1302 #[test]
1303 fn hash_sql_whitespace_only() {
1304 let h = hash_sql(" ");
1305 assert_ne!(h, hash_sql(""));
1306 }
1307
1308 #[test]
1309 fn hash_sql_very_long() {
1310 let long_sql = "SELECT ".to_string() + &"x".repeat(10_000);
1311 let h = hash_sql(&long_sql);
1312 assert_eq!(h, hash_sql(&long_sql));
1313 }
1314
1315 #[test]
1316 fn hash_sql_unicode() {
1317 let h = hash_sql("SELECT '\u{1F600}'");
1318 assert_ne!(h, hash_sql("SELECT 'x'"));
1319 }
1320
1321 #[test]
1326 fn notification_struct_fields() {
1327 let n = Notification {
1328 pid: 42,
1329 channel: "test_chan".to_owned(),
1330 payload: "hello".to_owned(),
1331 };
1332 assert_eq!(n.pid, 42);
1333 assert_eq!(n.channel, "test_chan");
1334 assert_eq!(n.payload, "hello");
1335 }
1336
1337 #[test]
1338 fn notification_clone() {
1339 let n = Notification {
1340 pid: 1,
1341 channel: "c".to_owned(),
1342 payload: "p".to_owned(),
1343 };
1344 let n2 = n.clone();
1345 assert_eq!(n2.pid, 1);
1346 assert_eq!(n2.channel, "c");
1347 }
1348
1349 #[test]
1350 fn notification_debug() {
1351 let n = Notification {
1352 pid: 1,
1353 channel: "c".to_owned(),
1354 payload: "p".to_owned(),
1355 };
1356 let dbg = format!("{n:?}");
1357 assert!(dbg.contains("Notification"));
1358 }
1359
1360 #[test]
1365 fn query_result_empty() {
1366 let result = QueryResult {
1367 all_col_offsets: vec![],
1368 num_cols: 0,
1369 columns: Arc::from(Vec::new()),
1370 affected_rows: 0,
1371 data_buf: None,
1372 };
1373 assert!(result.is_empty());
1374 assert_eq!(result.len(), 0);
1375 }
1376
1377 #[test]
1378 fn query_result_from_parts() {
1379 let result = QueryResult::from_parts(vec![(0, 4), (0, -1)], 2, Arc::from(Vec::new()), 5);
1380 assert_eq!(result.len(), 1);
1381 assert_eq!(result.num_cols, 2);
1382 assert_eq!(result.affected_rows, 5);
1383 }
1384
1385 #[test]
1386 fn query_result_affected_rows() {
1387 let result = QueryResult {
1388 all_col_offsets: vec![],
1389 num_cols: 0,
1390 columns: Arc::from(Vec::new()),
1391 affected_rows: 42,
1392 data_buf: None,
1393 };
1394 assert_eq!(result.affected_rows, 42);
1395 assert!(result.is_empty());
1396 }
1397
1398 fn make_data_row(columns: &[Option<&[u8]>]) -> Vec<u8> {
1405 let mut buf = Vec::new();
1406 buf.extend_from_slice(&(columns.len() as i16).to_be_bytes());
1407 for col in columns {
1408 match col {
1409 Some(data) => {
1410 buf.extend_from_slice(&(data.len() as i32).to_be_bytes());
1411 buf.extend_from_slice(data);
1412 }
1413 None => {
1414 buf.extend_from_slice(&(-1i32).to_be_bytes());
1415 }
1416 }
1417 }
1418 buf
1419 }
1420
1421 #[test]
1422 fn pg_data_row_get_i32() {
1423 let data = make_data_row(&[Some(&42i32.to_be_bytes())]);
1424 let row = PgDataRow::new(&data).unwrap();
1425 assert_eq!(row.get_i32(0), Some(42));
1426 assert_eq!(row.column_count(), 1);
1427 }
1428
1429 #[test]
1430 fn pg_data_row_get_i64() {
1431 let data = make_data_row(&[Some(&12345i64.to_be_bytes())]);
1432 let row = PgDataRow::new(&data).unwrap();
1433 assert_eq!(row.get_i64(0), Some(12345));
1434 }
1435
1436 #[test]
1437 fn pg_data_row_get_str() {
1438 let data = make_data_row(&[Some(b"hello")]);
1439 let row = PgDataRow::new(&data).unwrap();
1440 assert_eq!(row.get_str(0), Some("hello"));
1441 }
1442
1443 #[test]
1444 fn pg_data_row_get_bytes() {
1445 let data = make_data_row(&[Some(&[0xDE, 0xAD, 0xBE, 0xEF])]);
1446 let row = PgDataRow::new(&data).unwrap();
1447 assert_eq!(row.get_bytes(0), Some(&[0xDE, 0xAD, 0xBE, 0xEF][..]));
1448 }
1449
1450 #[test]
1451 fn pg_data_row_get_bool() {
1452 let data = make_data_row(&[Some(&[1u8])]);
1453 let row = PgDataRow::new(&data).unwrap();
1454 assert_eq!(row.get_bool(0), Some(true));
1455
1456 let data = make_data_row(&[Some(&[0u8])]);
1457 let row = PgDataRow::new(&data).unwrap();
1458 assert_eq!(row.get_bool(0), Some(false));
1459 }
1460
1461 #[test]
1462 fn pg_data_row_get_f64() {
1463 let data = make_data_row(&[Some(&3.14f64.to_be_bytes())]);
1464 let row = PgDataRow::new(&data).unwrap();
1465 assert!((row.get_f64(0).unwrap() - 3.14).abs() < 1e-10);
1466 }
1467
1468 #[test]
1469 fn pg_data_row_null_column() {
1470 let data = make_data_row(&[None]);
1471 let row = PgDataRow::new(&data).unwrap();
1472 assert!(row.is_null(0));
1473 assert_eq!(row.get_i32(0), None);
1474 assert_eq!(row.get_str(0), None);
1475 }
1476
1477 #[test]
1478 fn pg_data_row_multiple_columns() {
1479 let data = make_data_row(&[
1480 Some(&42i32.to_be_bytes()),
1481 Some(b"alice"),
1482 Some(b"alice@example.com"),
1483 Some(&[1u8]),
1484 Some(&3.14f64.to_be_bytes()),
1485 ]);
1486 let row = PgDataRow::new(&data).unwrap();
1487 assert_eq!(row.column_count(), 5);
1488 assert_eq!(row.get_i32(0), Some(42));
1489 assert_eq!(row.get_str(1), Some("alice"));
1490 assert_eq!(row.get_str(2), Some("alice@example.com"));
1491 assert_eq!(row.get_bool(3), Some(true));
1492 assert!((row.get_f64(4).unwrap() - 3.14).abs() < 1e-10);
1493 }
1494
1495 #[test]
1496 fn pg_data_row_mixed_null() {
1497 let data = make_data_row(&[Some(&42i32.to_be_bytes()), None, Some(b"text")]);
1498 let row = PgDataRow::new(&data).unwrap();
1499 assert_eq!(row.get_i32(0), Some(42));
1500 assert!(row.is_null(1));
1501 assert_eq!(row.get_str(1), None);
1502 assert_eq!(row.get_str(2), Some("text"));
1503 }
1504
1505 #[test]
1506 fn pg_data_row_empty() {
1507 let data = make_data_row(&[]);
1508 let row = PgDataRow::new(&data).unwrap();
1509 assert_eq!(row.column_count(), 0);
1510 }
1511
1512 #[test]
1513 fn pg_data_row_too_short() {
1514 let data = vec![0u8]; assert!(PgDataRow::new(&data).is_err());
1516 }
1517
1518 #[test]
1519 fn pg_data_row_truncated() {
1520 let mut data = Vec::new();
1522 data.extend_from_slice(&2i16.to_be_bytes());
1523 data.extend_from_slice(&4i32.to_be_bytes());
1524 data.extend_from_slice(&42i32.to_be_bytes());
1525 assert!(PgDataRow::new(&data).is_err());
1527 }
1528
1529 #[test]
1530 fn pg_data_row_get_i16() {
1531 let data = make_data_row(&[Some(&7i16.to_be_bytes())]);
1532 let row = PgDataRow::new(&data).unwrap();
1533 assert_eq!(row.get_i16(0), Some(7));
1534 }
1535
1536 #[test]
1537 fn pg_data_row_get_f32() {
1538 let data = make_data_row(&[Some(&2.5f32.to_be_bytes())]);
1539 let row = PgDataRow::new(&data).unwrap();
1540 assert!((row.get_f32(0).unwrap() - 2.5).abs() < 1e-6);
1541 }
1542
1543 #[test]
1544 fn pg_data_row_get_raw_null() {
1545 let data = make_data_row(&[None]);
1546 let row = PgDataRow::new(&data).unwrap();
1547 assert_eq!(row.get_raw(0), None);
1548 }
1549
1550 #[test]
1551 fn pg_data_row_get_raw_data() {
1552 let data = make_data_row(&[Some(&[1, 2, 3])]);
1553 let row = PgDataRow::new(&data).unwrap();
1554 assert_eq!(row.get_raw(0), Some(&[1u8, 2, 3][..]));
1555 }
1556
1557 #[test]
1558 fn pg_data_row_stack_alloc_16_columns() {
1559 let cols: Vec<Option<&[u8]>> = (0..16).map(|_| Some(&[0u8][..])).collect();
1561 let data = make_data_row(&cols);
1562 let row = PgDataRow::new(&data).unwrap();
1563 assert_eq!(row.column_count(), 16);
1564 for i in 0..16 {
1566 assert_eq!(row.get_raw(i), Some(&[0u8][..]));
1567 }
1568 }
1569
1570 #[test]
1575 fn inline_sequential_decode_five_columns() {
1576 let data = make_data_row(&[
1577 Some(&42i32.to_be_bytes()),
1578 Some(b"alice"),
1579 Some(b"alice@example.com"),
1580 Some(&[1u8]),
1581 Some(&3.14f64.to_be_bytes()),
1582 ]);
1583
1584 let mut pos: usize = 2; let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1589 pos += 4;
1590 assert_eq!(len, 4);
1591 let id = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1592 pos += len as usize;
1593 assert_eq!(id, 42);
1594
1595 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1597 pos += 4;
1598 assert_eq!(len, 5);
1599 let name = std::str::from_utf8(&data[pos..pos + len as usize]).unwrap();
1600 pos += len as usize;
1601 assert_eq!(name, "alice");
1602
1603 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1605 pos += 4;
1606 let email = std::str::from_utf8(&data[pos..pos + len as usize]).unwrap();
1607 pos += len as usize;
1608 assert_eq!(email, "alice@example.com");
1609
1610 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1612 pos += 4;
1613 assert_eq!(len, 1);
1614 let active = data[pos] != 0;
1615 pos += len as usize;
1616 assert!(active);
1617
1618 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1620 pos += 4;
1621 assert_eq!(len, 8);
1622 let score = f64::from_be_bytes([
1623 data[pos],
1624 data[pos + 1],
1625 data[pos + 2],
1626 data[pos + 3],
1627 data[pos + 4],
1628 data[pos + 5],
1629 data[pos + 6],
1630 data[pos + 7],
1631 ]);
1632 pos += len as usize;
1633 assert!((score - 3.14).abs() < 1e-10);
1634 assert_eq!(pos, data.len());
1635 }
1636
1637 #[test]
1639 fn inline_sequential_decode_with_nulls() {
1640 let data = make_data_row(&[
1641 Some(&42i32.to_be_bytes()),
1642 None, Some(b"text"),
1644 ]);
1645
1646 let mut pos: usize = 2;
1647
1648 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1650 pos += 4;
1651 let id = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1652 pos += len as usize;
1653 assert_eq!(id, 42);
1654
1655 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1657 pos += 4;
1658 let name: Option<&str> = if len < 0 {
1659 None
1660 } else {
1661 let s = std::str::from_utf8(&data[pos..pos + len as usize]).unwrap();
1662 pos += len as usize;
1663 Some(s)
1664 };
1665 assert!(name.is_none());
1666
1667 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1669 pos += 4;
1670 let txt = std::str::from_utf8(&data[pos..pos + len as usize]).unwrap();
1671 pos += len as usize;
1672 assert_eq!(txt, "text");
1673 assert_eq!(pos, data.len());
1674 }
1675
1676 #[test]
1678 fn inline_sequential_decode_all_scalar_types() {
1679 let data = make_data_row(&[
1680 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()), ]);
1687
1688 let mut pos: usize = 2;
1689
1690 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1692 pos += 4;
1693 let v_bool = data[pos] != 0;
1694 pos += len as usize;
1695 assert!(v_bool);
1696
1697 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1699 pos += 4;
1700 let v_i16 = i16::from_be_bytes([data[pos], data[pos + 1]]);
1701 pos += len as usize;
1702 assert_eq!(v_i16, 7);
1703
1704 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1706 pos += 4;
1707 let v_i32 = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1708 pos += len as usize;
1709 assert_eq!(v_i32, 42);
1710
1711 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1713 pos += 4;
1714 let v_i64 = i64::from_be_bytes([
1715 data[pos],
1716 data[pos + 1],
1717 data[pos + 2],
1718 data[pos + 3],
1719 data[pos + 4],
1720 data[pos + 5],
1721 data[pos + 6],
1722 data[pos + 7],
1723 ]);
1724 pos += len as usize;
1725 assert_eq!(v_i64, 12345);
1726
1727 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1729 pos += 4;
1730 let v_f32 = f32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1731 pos += len as usize;
1732 assert!((v_f32 - 2.5).abs() < 1e-6);
1733
1734 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1736 pos += 4;
1737 let v_f64 = f64::from_be_bytes([
1738 data[pos],
1739 data[pos + 1],
1740 data[pos + 2],
1741 data[pos + 3],
1742 data[pos + 4],
1743 data[pos + 5],
1744 data[pos + 6],
1745 data[pos + 7],
1746 ]);
1747 pos += len as usize;
1748 assert!((v_f64 - 3.14).abs() < 1e-10);
1749 assert_eq!(pos, data.len());
1750 }
1751
1752 #[test]
1754 fn pg_data_row_new_is_public() {
1755 let data = make_data_row(&[Some(&42i32.to_be_bytes())]);
1756 let row = PgDataRow::new(&data).unwrap();
1758 assert_eq!(row.get_i32(0), Some(42));
1759 }
1760
1761 #[test]
1763 fn inline_decode_matches_pgdatarow() {
1764 let data = make_data_row(&[
1765 Some(&99i32.to_be_bytes()),
1766 Some(b"hello world"),
1767 None,
1768 Some(&[0u8]),
1769 Some(&1.23f64.to_be_bytes()),
1770 ]);
1771
1772 let row = PgDataRow::new(&data).unwrap();
1774 let dr_i32 = row.get_i32(0);
1775 let dr_str = row.get_str(1);
1776 let dr_null = row.get_str(2);
1777 let dr_bool = row.get_bool(3);
1778 let dr_f64 = row.get_f64(4);
1779
1780 let mut pos: usize = 2;
1782
1783 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1784 pos += 4;
1785 let in_i32 = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1786 pos += len as usize;
1787
1788 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1789 pos += 4;
1790 let in_str = std::str::from_utf8(&data[pos..pos + len as usize]).unwrap();
1791 pos += len as usize;
1792
1793 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1794 pos += 4;
1795 let in_null: Option<&str> = if len < 0 { None } else { unreachable!() };
1796
1797 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1798 pos += 4;
1799 let in_bool = data[pos] != 0;
1800 pos += len as usize;
1801
1802 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1803 pos += 4;
1804 let in_f64 = f64::from_be_bytes([
1805 data[pos],
1806 data[pos + 1],
1807 data[pos + 2],
1808 data[pos + 3],
1809 data[pos + 4],
1810 data[pos + 5],
1811 data[pos + 6],
1812 data[pos + 7],
1813 ]);
1814 pos += len as usize;
1815
1816 assert_eq!(dr_i32, Some(in_i32));
1818 assert_eq!(dr_str, Some(in_str));
1819 assert_eq!(dr_null, in_null);
1820 assert_eq!(dr_bool, Some(in_bool));
1821 assert!((dr_f64.unwrap() - in_f64).abs() < 1e-15);
1822 assert_eq!(pos, data.len());
1823 }
1824
1825 #[test]
1830 fn pg_data_row_all_null_columns() {
1831 let data = make_data_row(&[None, None, None, None, None]);
1832 let row = PgDataRow::new(&data).unwrap();
1833 assert_eq!(row.column_count(), 5);
1834 for i in 0..5 {
1835 assert!(row.is_null(i), "column {i} should be null");
1836 assert_eq!(row.get_raw(i), None);
1837 assert_eq!(row.get_i32(i), None);
1838 assert_eq!(row.get_i64(i), None);
1839 assert_eq!(row.get_str(i), None);
1840 assert_eq!(row.get_bool(i), None);
1841 assert_eq!(row.get_f64(i), None);
1842 }
1843 }
1844
1845 #[test]
1846 fn pg_data_row_very_long_text() {
1847 let long_text = "x".repeat(2048);
1848 let data = make_data_row(&[Some(long_text.as_bytes())]);
1849 let row = PgDataRow::new(&data).unwrap();
1850 assert_eq!(row.get_str(0), Some(long_text.as_str()));
1851 }
1852
1853 #[test]
1854 fn pg_data_row_empty_text() {
1855 let data = make_data_row(&[Some(b"")]);
1856 let row = PgDataRow::new(&data).unwrap();
1857 assert!(!row.is_null(0));
1858 assert_eq!(row.get_str(0), Some(""));
1859 assert_eq!(row.get_bytes(0), Some(&[][..]));
1860 }
1861
1862 #[test]
1863 fn pg_data_row_20_columns_exceeds_inline() {
1864 let col_data: Vec<[u8; 4]> = (0..20).map(|i: i32| i.to_be_bytes()).collect();
1865 let cols: Vec<Option<&[u8]>> = col_data.iter().map(|b| Some(b.as_slice())).collect();
1866 let data = make_data_row(&cols);
1867 let row = PgDataRow::new(&data).unwrap();
1868 assert_eq!(row.column_count(), 20);
1869 for i in 0..20 {
1870 assert_eq!(row.get_i32(i), Some(i as i32));
1871 }
1872 }
1873
1874 #[test]
1875 fn pg_data_row_is_null_each_position() {
1876 let data = make_data_row(&[Some(&1i32.to_be_bytes()), None, Some(&3i32.to_be_bytes())]);
1878 let row = PgDataRow::new(&data).unwrap();
1879 assert!(!row.is_null(0));
1880 assert!(row.is_null(1));
1881 assert!(!row.is_null(2));
1882 }
1883
1884 #[test]
1885 fn pg_data_row_negative_column_count() {
1886 let data = (-1i16).to_be_bytes();
1887 assert!(PgDataRow::new(&data).is_err());
1888 }
1889
1890 #[test]
1891 fn pg_data_row_get_str_invalid_utf8() {
1892 let invalid_utf8 = &[0xFF, 0xFE, 0x80];
1893 let data = make_data_row(&[Some(invalid_utf8)]);
1894 let row = PgDataRow::new(&data).unwrap();
1895 assert_eq!(row.get_str(0), None);
1897 assert_eq!(row.get_bytes(0), Some(&[0xFF, 0xFE, 0x80][..]));
1898 }
1899
1900 #[test]
1901 fn pg_data_row_get_i32_wrong_length() {
1902 let data = make_data_row(&[Some(&7i16.to_be_bytes())]);
1904 let row = PgDataRow::new(&data).unwrap();
1905 assert_eq!(row.get_i32(0), None); assert_eq!(row.get_i16(0), Some(7)); }
1908
1909 #[test]
1910 fn pg_data_row_get_i64_wrong_length() {
1911 let data = make_data_row(&[Some(&42i32.to_be_bytes())]);
1913 let row = PgDataRow::new(&data).unwrap();
1914 assert_eq!(row.get_i64(0), None);
1915 }
1916
1917 #[test]
1918 fn pg_data_row_get_f64_wrong_length() {
1919 let data = make_data_row(&[Some(&2.5f32.to_be_bytes())]);
1920 let row = PgDataRow::new(&data).unwrap();
1921 assert_eq!(row.get_f64(0), None); }
1923
1924 #[test]
1925 fn pg_data_row_get_f32_wrong_length() {
1926 let data = make_data_row(&[Some(&3.14f64.to_be_bytes())]);
1927 let row = PgDataRow::new(&data).unwrap();
1928 assert_eq!(row.get_f32(0), None); }
1930
1931 #[test]
1932 fn pg_data_row_get_bool_wrong_length() {
1933 let data = make_data_row(&[Some(&42i32.to_be_bytes())]);
1935 let row = PgDataRow::new(&data).unwrap();
1936 assert_eq!(row.get_bool(0), None);
1937 }
1938
1939 #[test]
1940 fn pg_data_row_unicode_text() {
1941 let texts = [
1942 "\u{1F600}\u{1F4A9}\u{1F680}", "\u{4e16}\u{754c}", "\u{0645}\u{0631}\u{062D}", "\u{1F468}\u{200D}\u{1F469}", ];
1947 for text in &texts {
1948 let data = make_data_row(&[Some(text.as_bytes())]);
1949 let row = PgDataRow::new(&data).unwrap();
1950 assert_eq!(row.get_str(0), Some(*text));
1951 }
1952 }
1953
1954 #[test]
1955 fn pg_data_row_i32_boundary_values() {
1956 for &val in &[i32::MIN, -1, 0, 1, i32::MAX] {
1957 let data = make_data_row(&[Some(&val.to_be_bytes())]);
1958 let row = PgDataRow::new(&data).unwrap();
1959 assert_eq!(row.get_i32(0), Some(val), "failed for {val}");
1960 }
1961 }
1962
1963 #[test]
1964 fn pg_data_row_i64_boundary_values() {
1965 for &val in &[i64::MIN, -1, 0, 1, i64::MAX] {
1966 let data = make_data_row(&[Some(&val.to_be_bytes())]);
1967 let row = PgDataRow::new(&data).unwrap();
1968 assert_eq!(row.get_i64(0), Some(val), "failed for {val}");
1969 }
1970 }
1971
1972 #[test]
1973 fn pg_data_row_f64_special_values() {
1974 let data = make_data_row(&[Some(&f64::INFINITY.to_be_bytes())]);
1975 let row = PgDataRow::new(&data).unwrap();
1976 assert_eq!(row.get_f64(0), Some(f64::INFINITY));
1977
1978 let data = make_data_row(&[Some(&f64::NEG_INFINITY.to_be_bytes())]);
1979 let row = PgDataRow::new(&data).unwrap();
1980 assert_eq!(row.get_f64(0), Some(f64::NEG_INFINITY));
1981
1982 let data = make_data_row(&[Some(&f64::NAN.to_be_bytes())]);
1983 let row = PgDataRow::new(&data).unwrap();
1984 assert!(row.get_f64(0).unwrap().is_nan());
1985 }
1986
1987 #[test]
1988 fn pg_data_row_f32_special_values() {
1989 let data = make_data_row(&[Some(&f32::INFINITY.to_be_bytes())]);
1990 let row = PgDataRow::new(&data).unwrap();
1991 assert_eq!(row.get_f32(0), Some(f32::INFINITY));
1992
1993 let data = make_data_row(&[Some(&f32::NAN.to_be_bytes())]);
1994 let row = PgDataRow::new(&data).unwrap();
1995 assert!(row.get_f32(0).unwrap().is_nan());
1996 }
1997
1998 #[test]
1999 fn pg_data_row_i16_boundary_values() {
2000 for &val in &[i16::MIN, -1, 0, 1, i16::MAX] {
2001 let data = make_data_row(&[Some(&val.to_be_bytes())]);
2002 let row = PgDataRow::new(&data).unwrap();
2003 assert_eq!(row.get_i16(0), Some(val));
2004 }
2005 }
2006
2007 mod proptest_fuzz {
2008 use super::*;
2009 use proptest::prelude::*;
2010
2011 proptest! {
2012 #[test]
2013 fn config_from_url_never_panics(url in ".*") {
2014 let _ = Config::from_url(&url);
2015 }
2016
2017 #[test]
2018 fn url_decode_never_panics(s in ".*") {
2019 let _ = url_decode(&s);
2020 }
2021
2022 #[test]
2023 fn pg_data_row_new_never_panics(data in proptest::collection::vec(any::<u8>(), 0..8192)) {
2024 let _ = PgDataRow::new(&data);
2025 }
2026
2027 #[test]
2028 fn hash_sql_never_panics(sql in ".*") {
2029 let _ = hash_sql(&sql);
2030 }
2031 }
2032 }
2033}