1use std::sync::Arc;
7
8use rapidhash::quality::RapidHasher;
9
10use crate::arena::Arena;
11use crate::DriverError;
12
13#[derive(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 pub statement_cache_mode: StatementCacheMode,
38 pub ssl_root_cert: Option<String>,
41 pub ssl_cert: Option<String>,
43 pub ssl_key: Option<String>,
45}
46
47impl Drop for Config {
49 fn drop(&mut self) {
50 use zeroize::Zeroize;
51 self.password.zeroize();
52 }
53}
54
55impl std::fmt::Debug for Config {
58 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 f.debug_struct("Config")
60 .field("host", &self.host)
61 .field("port", &self.port)
62 .field("user", &self.user)
63 .field("password", &"[REDACTED]")
64 .field("database", &self.database)
65 .field("ssl", &self.ssl)
66 .field("statement_timeout_secs", &self.statement_timeout_secs)
67 .field("statement_cache_mode", &self.statement_cache_mode)
68 .field("ssl_root_cert", &self.ssl_root_cert)
69 .field("ssl_cert", &self.ssl_cert)
70 .field("ssl_key", &self.ssl_key)
71 .finish()
72 }
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum SslMode {
78 Disable,
80 Prefer,
82 Require,
84}
85
86#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
91pub enum StatementCacheMode {
92 #[default]
97 Named,
98 Disabled,
104}
105
106impl Config {
107 pub fn from_url(url: &str) -> Result<Self, DriverError> {
144 let url = url
145 .strip_prefix("postgres://")
146 .or_else(|| url.strip_prefix("postgresql://"))
147 .ok_or_else(|| DriverError::Protocol("URL must start with postgres://".into()))?;
148
149 let (userinfo, rest) = url
151 .split_once('@')
152 .ok_or_else(|| DriverError::Protocol("missing @ in connection URL".into()))?;
153
154 let (user, password) = userinfo.split_once(':').unwrap_or((userinfo, ""));
155
156 let (hostport, rest) = rest.split_once('/').unwrap_or((rest, ""));
158 let (database, params) = rest.split_once('?').unwrap_or((rest, ""));
159
160 let (host, port) = if let Some((h, p)) = hostport.split_once(':') {
161 let port = p
162 .parse::<u16>()
163 .map_err(|_| DriverError::Protocol(format!("invalid port: {p}")))?;
164 (h.to_owned(), port)
165 } else {
166 (hostport.to_owned(), 5432)
167 };
168
169 let mut ssl = SslMode::Prefer;
170 let mut statement_timeout_secs: u32 = 30;
171 let mut statement_cache_mode = StatementCacheMode::Named;
172 let mut host_override: Option<String> = None;
173 let mut ssl_root_cert: Option<String> = None;
174 let mut ssl_cert: Option<String> = None;
175 let mut ssl_key: Option<String> = None;
176 for param in params.split('&') {
177 if param.is_empty() {
178 continue;
179 }
180 if let Some(val) = param.strip_prefix("sslmode=") {
181 ssl = match val {
183 "disable" => SslMode::Disable,
184 "prefer" => SslMode::Prefer,
185 "require" => SslMode::Require,
186 _ => {
187 return Err(DriverError::Protocol(format!(
188 "unknown sslmode: '{val}' (expected: disable, prefer, require)"
189 )));
190 }
191 };
192 } else if let Some(val) = param.strip_prefix("statement_timeout=") {
193 statement_timeout_secs = val.parse::<u32>().unwrap_or(30);
194 } else if let Some(val) = param.strip_prefix("statement_cache=") {
195 statement_cache_mode = match val {
196 "named" => StatementCacheMode::Named,
197 "disabled" => StatementCacheMode::Disabled,
198 _ => {
199 return Err(DriverError::Protocol(format!(
200 "unknown statement_cache mode: '{val}' (expected: named, disabled)"
201 )));
202 }
203 };
204 } else if let Some(val) = param.strip_prefix("host=") {
205 host_override = Some(url_decode(val)?);
206 } else if let Some(val) = param.strip_prefix("sslrootcert=") {
207 ssl_root_cert = Some(url_decode(val)?);
208 } else if let Some(val) = param.strip_prefix("sslcert=") {
209 ssl_cert = Some(url_decode(val)?);
210 } else if let Some(val) = param.strip_prefix("sslkey=") {
211 ssl_key = Some(url_decode(val)?);
212 }
213 }
214
215 let final_host = if let Some(h) = host_override {
218 h
219 } else {
220 url_decode(&host)?
221 };
222
223 let config = Config {
224 host: final_host,
225 port,
226 user: url_decode(user)?,
227 password: url_decode(password)?,
228 database: if database.is_empty() {
229 url_decode(user)?
230 } else {
231 url_decode(database)?
232 },
233 ssl,
234 statement_timeout_secs,
235 statement_cache_mode,
236 ssl_root_cert,
237 ssl_cert,
238 ssl_key,
239 };
240 config.validate()?;
241 Ok(config)
242 }
243
244 pub fn validate(&self) -> Result<(), DriverError> {
249 if self.host.is_empty() {
250 return Err(DriverError::Protocol("host cannot be empty".into()));
251 }
252 if self.user.is_empty() {
253 return Err(DriverError::Protocol("user cannot be empty".into()));
254 }
255 if self.database.is_empty() {
256 return Err(DriverError::Protocol("database cannot be empty".into()));
257 }
258 Ok(())
259 }
260
261 pub fn host_is_uds(&self) -> bool {
266 self.host.starts_with('/')
267 }
268
269 pub fn uds_path(&self) -> String {
273 format!("{}/.s.PGSQL.{}", self.host, self.port)
274 }
275}
276
277fn url_decode(s: &str) -> Result<String, DriverError> {
287 let mut bytes = Vec::with_capacity(s.len());
288 let input = s.as_bytes();
289 let mut i = 0;
290 while i < input.len() {
291 if input[i] == b'%' {
292 if i + 2 >= input.len() {
293 return Err(DriverError::Protocol(format!(
294 "malformed percent-encoding in URL: '{s}'"
295 )));
296 }
297 let hi = hex_val(input[i + 1]).ok_or_else(|| {
298 DriverError::Protocol(format!(
299 "invalid hex digit '{}' in URL: '{s}'",
300 input[i + 1] as char
301 ))
302 })?;
303 let lo = hex_val(input[i + 2]).ok_or_else(|| {
304 DriverError::Protocol(format!(
305 "invalid hex digit '{}' in URL: '{s}'",
306 input[i + 2] as char
307 ))
308 })?;
309 bytes.push(hi * 16 + lo);
310 i += 3;
311 } else {
312 bytes.push(input[i]);
313 i += 1;
314 }
315 }
316 String::from_utf8(bytes)
317 .map_err(|_| DriverError::Protocol(format!("invalid UTF-8 in URL: '{s}'")))
318}
319
320fn hex_val(b: u8) -> Option<u8> {
321 match b {
322 b'0'..=b'9' => Some(b - b'0'),
323 b'a'..=b'f' => Some(b - b'a' + 10),
324 b'A'..=b'F' => Some(b - b'A' + 10),
325 _ => None,
326 }
327}
328
329pub(crate) enum StartupAction {
335 AuthOk,
336 AuthCleartext,
337 AuthMd5([u8; 4]),
338 AuthSasl(Vec<u8>),
339 ParameterStatus(Box<str>, Box<str>),
340 BackendKeyData(i32, i32),
341 ReadyForQuery(u8),
342 Error(String),
343 Notice,
344}
345
346#[derive(Debug, Clone)]
352pub struct ColumnDesc {
353 pub name: Box<str>,
355 pub type_oid: u32,
357 pub table_oid: u32,
359 pub type_size: i16,
361 pub column_id: i16,
363}
364
365#[derive(Debug, Clone)]
368pub struct PrepareResult {
369 pub columns: Vec<ColumnDesc>,
371 pub param_oids: Vec<u32>,
373}
374
375pub type SimpleRow = Vec<Option<String>>;
380
381#[derive(Debug, Clone)]
387pub struct Notification {
388 pub pid: i32,
390 pub channel: String,
392 pub payload: String,
394}
395
396pub struct QueryResult {
413 pub(crate) all_col_offsets: Vec<(usize, i32)>,
417 pub(crate) num_cols: usize,
419 pub(crate) columns: Arc<[ColumnDesc]>,
420 pub(crate) affected_rows: u64,
421 pub(crate) data_buf: Option<Vec<u8>>,
425}
426
427impl QueryResult {
428 pub fn from_parts(
432 all_col_offsets: Vec<(usize, i32)>,
433 num_cols: usize,
434 columns: Arc<[ColumnDesc]>,
435 affected_rows: u64,
436 ) -> Self {
437 Self {
438 all_col_offsets,
439 num_cols,
440 columns,
441 affected_rows,
442 data_buf: None,
443 }
444 }
445
446 pub fn from_parts_with_buf(
448 all_col_offsets: Vec<(usize, i32)>,
449 num_cols: usize,
450 columns: Arc<[ColumnDesc]>,
451 affected_rows: u64,
452 data_buf: Vec<u8>,
453 ) -> Self {
454 Self {
455 all_col_offsets,
456 num_cols,
457 columns,
458 affected_rows,
459 data_buf: if data_buf.is_empty() {
460 None
461 } else {
462 Some(data_buf)
463 },
464 }
465 }
466
467 pub fn len(&self) -> usize {
469 if self.num_cols == 0 {
470 return 0;
471 }
472 self.all_col_offsets.len() / self.num_cols
473 }
474
475 pub fn is_empty(&self) -> bool {
477 self.all_col_offsets.is_empty()
478 }
479
480 pub fn affected_rows(&self) -> u64 {
482 self.affected_rows
483 }
484
485 pub fn columns(&self) -> &[ColumnDesc] {
487 &self.columns
488 }
489
490 pub fn row<'a>(&'a self, idx: usize, arena: &'a Arena) -> Row<'a> {
493 let start = idx * self.num_cols;
494 let end = start + self.num_cols;
495 Row {
496 data: self.data_buf.as_deref(),
497 arena,
498 col_offsets: &self.all_col_offsets[start..end],
499 columns: &self.columns,
500 }
501 }
502
503 pub fn take_col_offsets(&mut self) -> Vec<(usize, i32)> {
508 std::mem::take(&mut self.all_col_offsets)
509 }
510
511 pub fn take_data_buf(&mut self) -> Option<Vec<u8>> {
513 self.data_buf.take()
514 }
515
516 pub fn rows<'a>(&'a self, arena: &'a Arena) -> impl Iterator<Item = Row<'a>> {
518 let num_cols = self.num_cols;
519 let columns = &self.columns;
520 let data = self.data_buf.as_deref();
521 self.all_col_offsets
522 .chunks(num_cols.max(1))
523 .map(move |chunk| Row {
524 data,
525 arena,
526 col_offsets: chunk,
527 columns,
528 })
529 }
530}
531
532pub struct Row<'a> {
543 data: Option<&'a [u8]>,
546 arena: &'a Arena,
547 col_offsets: &'a [(usize, i32)],
548 columns: &'a [ColumnDesc],
549}
550
551impl<'a> Row<'a> {
552 #[inline]
554 pub fn get_raw(&self, idx: usize) -> Option<&'a [u8]> {
555 let (offset, len) = self.col_offsets[idx];
556 if len < 0 {
557 None
558 } else if let Some(buf) = self.data {
559 Some(&buf[offset..offset + len as usize])
560 } else {
561 Some(self.arena.get(offset, len as usize))
562 }
563 }
564
565 #[inline]
567 pub fn is_null(&self, idx: usize) -> bool {
568 self.col_offsets[idx].1 < 0
569 }
570
571 #[inline]
573 pub fn column_count(&self) -> usize {
574 self.col_offsets.len()
575 }
576
577 #[inline]
579 pub fn get_bool(&self, idx: usize) -> Option<bool> {
580 self.get_raw(idx)
581 .and_then(|data| crate::codec::decode_bool(data).ok())
582 }
583
584 #[inline]
586 pub fn get_i16(&self, idx: usize) -> Option<i16> {
587 self.get_raw(idx)
588 .and_then(|data| crate::codec::decode_i16(data).ok())
589 }
590
591 #[inline]
593 pub fn get_i32(&self, idx: usize) -> Option<i32> {
594 self.get_raw(idx)
595 .and_then(|data| crate::codec::decode_i32(data).ok())
596 }
597
598 #[inline]
600 pub fn get_i64(&self, idx: usize) -> Option<i64> {
601 self.get_raw(idx)
602 .and_then(|data| crate::codec::decode_i64(data).ok())
603 }
604
605 #[inline]
607 pub fn get_f32(&self, idx: usize) -> Option<f32> {
608 self.get_raw(idx)
609 .and_then(|data| crate::codec::decode_f32(data).ok())
610 }
611
612 #[inline]
614 pub fn get_f64(&self, idx: usize) -> Option<f64> {
615 self.get_raw(idx)
616 .and_then(|data| crate::codec::decode_f64(data).ok())
617 }
618
619 #[inline]
621 pub fn get_str(&self, idx: usize) -> Option<&'a str> {
622 self.get_raw(idx)
623 .and_then(|data| crate::codec::decode_str(data).ok())
624 }
625
626 #[inline]
628 pub fn get_bytes(&self, idx: usize) -> Option<&'a [u8]> {
629 self.get_raw(idx)
630 }
631
632 #[inline]
634 pub fn column_name(&self, idx: usize) -> &str {
635 &self.columns[idx].name
636 }
637
638 #[inline]
640 pub fn column_type_oid(&self, idx: usize) -> u32 {
641 self.columns[idx].type_oid
642 }
643}
644
645pub struct PgDataRow<'a> {
658 data: &'a [u8],
659 offsets: smallvec::SmallVec<[(usize, i32); 16]>,
662}
663
664impl<'a> PgDataRow<'a> {
665 pub fn new(data: &'a [u8]) -> Result<Self, DriverError> {
670 if data.len() < 2 {
671 return Err(DriverError::Protocol("DataRow too short".into()));
672 }
673 let num_cols = i16::from_be_bytes([data[0], data[1]]);
674 if num_cols < 0 {
675 return Err(DriverError::Protocol(
676 "DataRow: negative column count".into(),
677 ));
678 }
679 let num_cols = num_cols as usize;
680 let mut offsets = smallvec::SmallVec::<[(usize, i32); 16]>::with_capacity(num_cols);
681 let mut pos = 2usize;
682 for _ in 0..num_cols {
683 if pos + 4 > data.len() {
684 return Err(DriverError::Protocol("DataRow truncated".into()));
685 }
686 let col_len =
687 i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
688 pos += 4;
689 offsets.push((pos, col_len));
690 if col_len > 0 {
691 pos += col_len as usize;
692 }
693 }
694 Ok(Self { data, offsets })
695 }
696
697 #[inline]
699 pub fn get_raw(&self, idx: usize) -> Option<&'a [u8]> {
700 let (offset, len) = self.offsets[idx];
701 if len < 0 {
702 None
703 } else {
704 Some(&self.data[offset..offset + len as usize])
705 }
706 }
707
708 #[inline]
710 pub fn is_null(&self, idx: usize) -> bool {
711 self.offsets[idx].1 < 0
712 }
713
714 #[inline]
716 pub fn column_count(&self) -> usize {
717 self.offsets.len()
718 }
719
720 #[inline]
722 pub fn get_bool(&self, idx: usize) -> Option<bool> {
723 self.get_raw(idx)
724 .and_then(|data| crate::codec::decode_bool(data).ok())
725 }
726
727 #[inline]
729 pub fn get_i16(&self, idx: usize) -> Option<i16> {
730 self.get_raw(idx)
731 .and_then(|data| crate::codec::decode_i16(data).ok())
732 }
733
734 #[inline]
736 pub fn get_i32(&self, idx: usize) -> Option<i32> {
737 self.get_raw(idx)
738 .and_then(|data| crate::codec::decode_i32(data).ok())
739 }
740
741 #[inline]
743 pub fn get_i64(&self, idx: usize) -> Option<i64> {
744 self.get_raw(idx)
745 .and_then(|data| crate::codec::decode_i64(data).ok())
746 }
747
748 #[inline]
750 pub fn get_f32(&self, idx: usize) -> Option<f32> {
751 self.get_raw(idx)
752 .and_then(|data| crate::codec::decode_f32(data).ok())
753 }
754
755 #[inline]
757 pub fn get_f64(&self, idx: usize) -> Option<f64> {
758 self.get_raw(idx)
759 .and_then(|data| crate::codec::decode_f64(data).ok())
760 }
761
762 #[inline]
764 pub fn get_str(&self, idx: usize) -> Option<&'a str> {
765 self.get_raw(idx)
766 .and_then(|data| crate::codec::decode_str(data).ok())
767 }
768
769 #[inline]
771 pub fn get_bytes(&self, idx: usize) -> Option<&'a [u8]> {
772 self.get_raw(idx)
773 }
774}
775
776pub fn hash_sql(sql: &str) -> u64 {
793 use std::hash::{Hash, Hasher};
794 let mut hasher = RapidHasher::default();
795 sql.hash(&mut hasher);
796 hasher.finish()
797}
798
799#[cfg(test)]
804#[allow(clippy::approx_constant)]
805mod tests {
806 use super::*;
807
808 #[test]
813 fn config_parse_full_url() {
814 let cfg = Config::from_url("postgres://user:pass@localhost:5432/mydb").unwrap();
815 assert_eq!(cfg.user, "user");
816 assert_eq!(cfg.password, "pass");
817 assert_eq!(cfg.host, "localhost");
818 assert_eq!(cfg.port, 5432);
819 assert_eq!(cfg.database, "mydb");
820 }
821
822 #[test]
823 fn config_parse_default_port() {
824 let cfg = Config::from_url("postgres://user:pass@localhost/mydb").unwrap();
825 assert_eq!(cfg.port, 5432);
826 }
827
828 #[test]
829 fn config_parse_no_password() {
830 let cfg = Config::from_url("postgres://user@localhost/mydb").unwrap();
831 assert_eq!(cfg.user, "user");
832 assert_eq!(cfg.password, "");
833 }
834
835 #[test]
836 fn config_parse_empty_database() {
837 let cfg = Config::from_url("postgres://user:pass@localhost").unwrap();
838 assert_eq!(cfg.database, "user");
840 }
841
842 #[test]
843 fn config_parse_sslmode() {
844 let cfg = Config::from_url("postgres://user:pass@localhost/db?sslmode=require").unwrap();
845 assert_eq!(cfg.ssl, SslMode::Require);
846 }
847
848 #[test]
849 fn config_parse_percent_encoding() {
850 let cfg = Config::from_url("postgres://user%40domain:p%40ss@localhost/db").unwrap();
851 assert_eq!(cfg.user, "user@domain");
852 assert_eq!(cfg.password, "p@ss");
853 }
854
855 #[test]
856 fn config_rejects_bad_scheme() {
857 let result = Config::from_url("mysql://user:pass@localhost/db");
858 assert!(result.is_err());
859 }
860
861 #[test]
863 fn config_rejects_unknown_sslmode() {
864 let result = Config::from_url("postgres://user:pass@localhost/db?sslmode=requre");
865 assert!(result.is_err(), "typo 'requre' should be rejected");
866 let result = Config::from_url("postgres://user:pass@localhost/db?sslmode=REQUIRE");
867 assert!(result.is_err(), "uppercase should be rejected");
868 let result = Config::from_url("postgres://user:pass@localhost/db?sslmode=bogus");
869 assert!(result.is_err(), "bogus value should be rejected");
870 }
871
872 #[test]
874 fn config_accepts_valid_sslmodes() {
875 let cfg = Config::from_url("postgres://user:pass@localhost/db?sslmode=disable").unwrap();
876 assert_eq!(cfg.ssl, SslMode::Disable);
877 let cfg = Config::from_url("postgres://user:pass@localhost/db?sslmode=prefer").unwrap();
878 assert_eq!(cfg.ssl, SslMode::Prefer);
879 let cfg = Config::from_url("postgres://user:pass@localhost/db?sslmode=require").unwrap();
880 assert_eq!(cfg.ssl, SslMode::Require);
881 }
882
883 #[test]
885 fn config_parse_postgresql_scheme() {
886 let cfg = Config::from_url("postgresql://user:pass@localhost:5432/mydb").unwrap();
887 assert_eq!(cfg.user, "user");
888 assert_eq!(cfg.password, "pass");
889 assert_eq!(cfg.host, "localhost");
890 assert_eq!(cfg.port, 5432);
891 assert_eq!(cfg.database, "mydb");
892 }
893
894 #[test]
896 fn config_parse_no_password_standalone() {
897 let cfg = Config::from_url("postgres://admin@db.example.com/myapp").unwrap();
898 assert_eq!(cfg.user, "admin");
899 assert_eq!(cfg.password, "");
900 assert_eq!(cfg.host, "db.example.com");
901 assert_eq!(cfg.database, "myapp");
902 }
903
904 #[test]
906 fn config_empty_database_falls_back_to_user() {
907 let cfg = Config::from_url("postgres://testuser:pass@localhost").unwrap();
908 assert_eq!(cfg.database, "testuser");
909 }
910
911 #[test]
913 fn config_unknown_sslmode_error() {
914 let result = Config::from_url("postgres://u:p@h/d?sslmode=verify-full");
915 assert!(result.is_err());
916 let err = result.unwrap_err().to_string();
917 assert!(
918 err.contains("unknown sslmode"),
919 "should describe unknown sslmode: {err}"
920 );
921 }
922
923 #[test]
925 fn config_multiple_query_params() {
926 let cfg = Config::from_url(
927 "postgres://user:pass@localhost/db?sslmode=disable&statement_timeout=60",
928 )
929 .unwrap();
930 assert_eq!(cfg.ssl, SslMode::Disable);
931 assert_eq!(cfg.statement_timeout_secs, 60);
932 }
933
934 #[test]
936 fn config_validate_empty_host() {
937 let cfg = Config {
938 host: String::new(),
939 port: 5432,
940 user: "user".into(),
941 password: "pass".into(),
942 database: "db".into(),
943 ssl: SslMode::Disable,
944 statement_timeout_secs: 30,
945 statement_cache_mode: StatementCacheMode::Named,
946 ssl_root_cert: None,
947 ssl_cert: None,
948 ssl_key: None,
949 };
950 assert!(cfg.validate().is_err());
951 }
952
953 #[test]
955 fn config_validate_empty_user() {
956 let cfg = Config {
957 host: "localhost".into(),
958 port: 5432,
959 user: String::new(),
960 password: "pass".into(),
961 database: "db".into(),
962 ssl: SslMode::Disable,
963 statement_timeout_secs: 30,
964 statement_cache_mode: StatementCacheMode::Named,
965 ssl_root_cert: None,
966 ssl_cert: None,
967 ssl_key: None,
968 };
969 assert!(cfg.validate().is_err());
970 }
971
972 #[test]
974 fn config_validate_empty_database() {
975 let cfg = Config {
976 host: "localhost".into(),
977 port: 5432,
978 user: "user".into(),
979 password: "pass".into(),
980 database: String::new(),
981 ssl: SslMode::Disable,
982 statement_timeout_secs: 30,
983 statement_cache_mode: StatementCacheMode::Named,
984 ssl_root_cert: None,
985 ssl_cert: None,
986 ssl_key: None,
987 };
988 assert!(cfg.validate().is_err());
989 }
990
991 #[test]
993 fn config_missing_at_sign() {
994 let result = Config::from_url("postgres://userpasslocalhost/db");
995 assert!(result.is_err());
996 }
997
998 #[test]
1000 fn config_custom_port() {
1001 let cfg = Config::from_url("postgres://user:pass@localhost:5433/db").unwrap();
1002 assert_eq!(cfg.port, 5433);
1003 }
1004
1005 #[test]
1007 fn config_invalid_port() {
1008 let result = Config::from_url("postgres://user:pass@localhost:notaport/db");
1009 assert!(result.is_err());
1010 }
1011
1012 #[cfg(not(feature = "tls"))]
1014 #[test]
1015 fn config_sslmode_require_without_tls_feature() {
1016 let cfg = Config::from_url("postgres://user:pass@localhost/db?sslmode=require").unwrap();
1019 assert_eq!(cfg.ssl, SslMode::Require);
1020 }
1021
1022 #[test]
1023 fn config_statement_timeout_default() {
1024 let cfg = Config::from_url("postgres://user:pass@localhost/db").unwrap();
1025 assert_eq!(cfg.statement_timeout_secs, 30);
1026 }
1027
1028 #[test]
1029 fn config_statement_timeout_custom() {
1030 let cfg =
1031 Config::from_url("postgres://user:pass@localhost/db?statement_timeout=120").unwrap();
1032 assert_eq!(cfg.statement_timeout_secs, 120);
1033 }
1034
1035 #[test]
1036 fn config_statement_timeout_zero() {
1037 let cfg =
1038 Config::from_url("postgres://user:pass@localhost/db?statement_timeout=0").unwrap();
1039 assert_eq!(cfg.statement_timeout_secs, 0);
1040 }
1041
1042 #[test]
1043 fn config_statement_timeout_invalid_falls_back() {
1044 let cfg =
1045 Config::from_url("postgres://user:pass@localhost/db?statement_timeout=notanumber")
1046 .unwrap();
1047 assert_eq!(cfg.statement_timeout_secs, 30); }
1049
1050 #[test]
1055 fn parse_statement_cache_default() {
1056 let cfg = Config::from_url("postgres://user:pass@localhost/db").unwrap();
1057 assert_eq!(cfg.statement_cache_mode, StatementCacheMode::Named);
1058 }
1059
1060 #[test]
1061 fn parse_statement_cache_named() {
1062 let cfg =
1063 Config::from_url("postgres://user:pass@localhost/db?statement_cache=named").unwrap();
1064 assert_eq!(cfg.statement_cache_mode, StatementCacheMode::Named);
1065 }
1066
1067 #[test]
1068 fn parse_statement_cache_disabled() {
1069 let cfg =
1070 Config::from_url("postgres://user:pass@localhost/db?statement_cache=disabled").unwrap();
1071 assert_eq!(cfg.statement_cache_mode, StatementCacheMode::Disabled);
1072 }
1073
1074 #[test]
1075 fn parse_statement_cache_invalid() {
1076 let result = Config::from_url("postgres://user:pass@localhost/db?statement_cache=off");
1077 assert!(result.is_err(), "invalid value 'off' should be rejected");
1078 let result = Config::from_url("postgres://user:pass@localhost/db?statement_cache=DISABLED");
1079 assert!(result.is_err(), "uppercase should be rejected");
1080 let result = Config::from_url("postgres://user:pass@localhost/db?statement_cache=bogus");
1081 assert!(result.is_err(), "bogus value should be rejected");
1082 }
1083
1084 #[test]
1085 fn parse_statement_cache_with_other_params() {
1086 let cfg = Config::from_url(
1087 "postgres://user:pass@localhost/db?sslmode=disable&statement_cache=disabled&statement_timeout=60",
1088 )
1089 .unwrap();
1090 assert_eq!(cfg.statement_cache_mode, StatementCacheMode::Disabled);
1091 assert_eq!(cfg.ssl, SslMode::Disable);
1092 assert_eq!(cfg.statement_timeout_secs, 60);
1093 }
1094
1095 #[test]
1096 fn statement_cache_mode_default_is_named() {
1097 assert_eq!(StatementCacheMode::default(), StatementCacheMode::Named);
1098 }
1099
1100 #[test]
1105 fn parse_ssl_root_cert() {
1106 let cfg = Config::from_url("postgres://user:pass@localhost/db?sslrootcert=/path/to/ca.pem")
1107 .unwrap();
1108 assert_eq!(cfg.ssl_root_cert.as_deref(), Some("/path/to/ca.pem"));
1109 assert_eq!(cfg.ssl_cert, None);
1110 assert_eq!(cfg.ssl_key, None);
1111 }
1112
1113 #[test]
1114 fn parse_ssl_cert_and_key() {
1115 let cfg = Config::from_url(
1116 "postgres://user:pass@localhost/db?sslcert=/path/to/client.pem&sslkey=/path/to/client.key",
1117 )
1118 .unwrap();
1119 assert_eq!(cfg.ssl_root_cert, None);
1120 assert_eq!(cfg.ssl_cert.as_deref(), Some("/path/to/client.pem"));
1121 assert_eq!(cfg.ssl_key.as_deref(), Some("/path/to/client.key"));
1122 }
1123
1124 #[test]
1125 fn parse_ssl_all_tls_params() {
1126 let cfg = Config::from_url(
1127 "postgres://user:pass@localhost/db?sslmode=require&sslrootcert=/ca.pem&sslcert=/client.pem&sslkey=/client.key",
1128 )
1129 .unwrap();
1130 assert_eq!(cfg.ssl, SslMode::Require);
1131 assert_eq!(cfg.ssl_root_cert.as_deref(), Some("/ca.pem"));
1132 assert_eq!(cfg.ssl_cert.as_deref(), Some("/client.pem"));
1133 assert_eq!(cfg.ssl_key.as_deref(), Some("/client.key"));
1134 }
1135
1136 #[test]
1137 fn parse_ssl_paths_percent_encoded() {
1138 let cfg = Config::from_url("postgres://user:pass@localhost/db?sslrootcert=%2Ftmp%2Fca.pem")
1140 .unwrap();
1141 assert_eq!(cfg.ssl_root_cert.as_deref(), Some("/tmp/ca.pem"));
1142 }
1143
1144 #[test]
1145 fn parse_ssl_params_default_none() {
1146 let cfg = Config::from_url("postgres://user:pass@localhost/db").unwrap();
1147 assert_eq!(cfg.ssl_root_cert, None);
1148 assert_eq!(cfg.ssl_cert, None);
1149 assert_eq!(cfg.ssl_key, None);
1150 }
1151
1152 #[test]
1153 fn config_uds_path_format() {
1154 let cfg = Config::from_url("postgres://user@localhost/db?host=/tmp").unwrap();
1155 assert_eq!(cfg.uds_path(), "/tmp/.s.PGSQL.5432");
1156 }
1157
1158 #[test]
1159 fn config_uds_path_custom_port() {
1160 let cfg = Config::from_url("postgres://user@localhost:5433/db?host=/tmp").unwrap();
1161 assert_eq!(cfg.uds_path(), "/tmp/.s.PGSQL.5433");
1162 }
1163
1164 #[test]
1169 fn config_host_is_uds_absolute_path() {
1170 let cfg = Config {
1171 host: "/tmp".into(),
1172 port: 5432,
1173 user: "user".into(),
1174 password: "".into(),
1175 database: "db".into(),
1176 ssl: SslMode::Disable,
1177 statement_timeout_secs: 30,
1178 statement_cache_mode: StatementCacheMode::Named,
1179 ssl_root_cert: None,
1180 ssl_cert: None,
1181 ssl_key: None,
1182 };
1183 assert!(cfg.host_is_uds());
1184 assert_eq!(cfg.uds_path(), "/tmp/.s.PGSQL.5432");
1185 }
1186
1187 #[test]
1188 fn config_host_is_uds_var_run() {
1189 let cfg = Config {
1190 host: "/var/run/postgresql".into(),
1191 port: 5433,
1192 user: "user".into(),
1193 password: "".into(),
1194 database: "db".into(),
1195 ssl: SslMode::Disable,
1196 statement_timeout_secs: 30,
1197 statement_cache_mode: StatementCacheMode::Named,
1198 ssl_root_cert: None,
1199 ssl_cert: None,
1200 ssl_key: None,
1201 };
1202 assert!(cfg.host_is_uds());
1203 assert_eq!(cfg.uds_path(), "/var/run/postgresql/.s.PGSQL.5433");
1204 }
1205
1206 #[test]
1207 fn config_host_is_not_uds_for_hostname() {
1208 let cfg = Config {
1209 host: "localhost".into(),
1210 port: 5432,
1211 user: "user".into(),
1212 password: "".into(),
1213 database: "db".into(),
1214 ssl: SslMode::Disable,
1215 statement_timeout_secs: 30,
1216 statement_cache_mode: StatementCacheMode::Named,
1217 ssl_root_cert: None,
1218 ssl_cert: None,
1219 ssl_key: None,
1220 };
1221 assert!(!cfg.host_is_uds());
1222 }
1223
1224 #[test]
1225 fn config_host_is_not_uds_for_ip() {
1226 let cfg = Config {
1227 host: "127.0.0.1".into(),
1228 port: 5432,
1229 user: "user".into(),
1230 password: "".into(),
1231 database: "db".into(),
1232 ssl: SslMode::Disable,
1233 statement_timeout_secs: 30,
1234 statement_cache_mode: StatementCacheMode::Named,
1235 ssl_root_cert: None,
1236 ssl_cert: None,
1237 ssl_key: None,
1238 };
1239 assert!(!cfg.host_is_uds());
1240 }
1241
1242 #[test]
1243 fn config_parse_uds_host_query_param() {
1244 let cfg = Config::from_url("postgres://user@localhost/mydb?host=/tmp").unwrap();
1245 assert_eq!(cfg.host, "/tmp");
1246 assert!(cfg.host_is_uds());
1247 assert_eq!(cfg.uds_path(), "/tmp/.s.PGSQL.5432");
1248 assert_eq!(cfg.database, "mydb");
1249 assert_eq!(cfg.user, "user");
1250 }
1251
1252 #[test]
1253 fn config_parse_uds_host_query_param_custom_port() {
1254 let cfg = Config::from_url("postgres://user@localhost:5433/mydb?host=/var/run/postgresql")
1255 .unwrap();
1256 assert_eq!(cfg.host, "/var/run/postgresql");
1257 assert_eq!(cfg.port, 5433);
1258 assert_eq!(cfg.uds_path(), "/var/run/postgresql/.s.PGSQL.5433");
1259 }
1260
1261 #[test]
1262 fn config_parse_uds_host_with_other_params() {
1263 let cfg = Config::from_url(
1264 "postgres://user@localhost/db?host=/tmp&sslmode=disable&statement_timeout=60",
1265 )
1266 .unwrap();
1267 assert_eq!(cfg.host, "/tmp");
1268 assert!(cfg.host_is_uds());
1269 assert_eq!(cfg.ssl, SslMode::Disable);
1270 assert_eq!(cfg.statement_timeout_secs, 60);
1271 }
1272
1273 #[test]
1274 fn config_parse_uds_host_percent_encoded() {
1275 let cfg = Config::from_url("postgres://user@localhost/db?host=%2Ftmp").unwrap();
1277 assert_eq!(cfg.host, "/tmp");
1278 assert!(cfg.host_is_uds());
1279 }
1280
1281 #[test]
1282 fn config_parse_tcp_host_not_overridden_without_param() {
1283 let cfg = Config::from_url("postgres://user@myserver/db").unwrap();
1285 assert_eq!(cfg.host, "myserver");
1286 assert!(!cfg.host_is_uds());
1287 }
1288
1289 #[test]
1290 fn config_parse_uds_host_overrides_url_hostname() {
1291 let cfg = Config::from_url("postgres://user@db.example.com/mydb?host=/var/run/postgresql")
1293 .unwrap();
1294 assert_eq!(cfg.host, "/var/run/postgresql");
1295 assert!(cfg.host_is_uds());
1296 }
1297
1298 #[test]
1299 fn config_parse_uds_empty_url_host() {
1300 let cfg = Config::from_url("postgres://user@/mydb?host=/tmp").unwrap();
1302 assert_eq!(cfg.host, "/tmp");
1303 assert!(cfg.host_is_uds());
1304 assert_eq!(cfg.database, "mydb");
1305 }
1306
1307 #[test]
1312 fn url_decode_works() {
1313 assert_eq!(url_decode("hello%20world").unwrap(), "hello world");
1314 assert_eq!(url_decode("no%20escape").unwrap(), "no escape");
1315 assert_eq!(url_decode("plain").unwrap(), "plain");
1316 assert_eq!(url_decode("a%40b").unwrap(), "a@b");
1317 }
1318
1319 #[test]
1320 fn url_decode_malformed_percent_trailing() {
1321 let result = url_decode("abc%2");
1323 assert!(result.is_err(), "truncated %2 should error");
1324 }
1325
1326 #[test]
1327 fn url_decode_malformed_percent_no_digits() {
1328 let result = url_decode("abc%");
1330 assert!(result.is_err(), "bare % at end should error");
1331 }
1332
1333 #[test]
1334 fn url_decode_invalid_hex_digit() {
1335 let result = url_decode("abc%GG");
1337 assert!(result.is_err(), "%GG should error");
1338 }
1339
1340 #[test]
1341 fn url_decode_invalid_hex_second_digit() {
1342 let result = url_decode("abc%2Z");
1344 assert!(result.is_err(), "%2Z should error");
1345 }
1346
1347 #[test]
1349 fn url_decode_invalid_utf8_percent() {
1350 let result = url_decode("%80%81");
1352 assert!(result.is_err(), "invalid UTF-8 bytes should error");
1353 }
1354
1355 #[test]
1357 fn url_decode_percent_everywhere() {
1358 assert_eq!(url_decode("%41%42%43").unwrap(), "ABC");
1359 assert_eq!(url_decode("%61").unwrap(), "a");
1360 assert_eq!(url_decode("x%2Fy%2Fz").unwrap(), "x/y/z");
1361 }
1362
1363 #[test]
1365 fn url_decode_bare_percent_middle() {
1366 assert!(url_decode("a%b").is_err(), "bare % in middle should error");
1367 }
1368
1369 #[test]
1371 fn url_decode_multibyte_utf8() {
1372 let result = url_decode("caf%C3%A9").unwrap();
1373 assert_eq!(result, "caf\u{00e9}"); }
1375
1376 #[test]
1378 fn url_decode_invalid_percent_zz() {
1379 let result = url_decode("abc%ZZ");
1380 assert!(result.is_err(), "%ZZ should error");
1381 }
1382
1383 #[test]
1385 fn url_decode_truncated_percent_trailing() {
1386 let result = url_decode("abc%");
1387 assert!(result.is_err(), "trailing % should error");
1388 }
1389
1390 #[test]
1392 fn url_decode_invalid_utf8() {
1393 let result = url_decode("%80");
1395 assert!(result.is_err(), "invalid UTF-8 should error");
1396 }
1397
1398 #[test]
1399 fn url_decode_empty_string() {
1400 assert_eq!(url_decode("").unwrap(), "");
1401 }
1402
1403 #[test]
1404 fn url_decode_no_encoding() {
1405 assert_eq!(url_decode("hello").unwrap(), "hello");
1406 }
1407
1408 #[test]
1409 fn url_decode_all_ascii_hex() {
1410 assert_eq!(url_decode("%2F").unwrap(), "/");
1412 assert_eq!(url_decode("%2f").unwrap(), "/");
1413 }
1414
1415 #[test]
1419 fn config_unicode_password() {
1420 let cfg =
1422 Config::from_url("postgres://user:%D0%BF%D0%B0%D1%80%D0%BE%D0%BB%D1%8C@localhost/db")
1423 .unwrap();
1424 assert_eq!(cfg.user, "user");
1425 assert_eq!(
1426 cfg.password,
1427 "\u{043F}\u{0430}\u{0440}\u{043E}\u{043B}\u{044C}"
1428 ); assert_eq!(cfg.host, "localhost");
1430 assert_eq!(cfg.database, "db");
1431 }
1432
1433 #[test]
1435 fn config_port_zero() {
1436 let cfg = Config::from_url("postgres://user:pass@localhost:0/db").unwrap();
1437 assert_eq!(cfg.port, 0);
1438 }
1439
1440 #[test]
1442 fn config_port_max() {
1443 let cfg = Config::from_url("postgres://user:pass@localhost:65535/db").unwrap();
1444 assert_eq!(cfg.port, 65535);
1445 }
1446
1447 #[test]
1449 fn config_port_overflow() {
1450 let result = Config::from_url("postgres://user:pass@localhost:65536/db");
1451 assert!(result.is_err(), "port 65536 exceeds u16 max");
1452 }
1453
1454 #[test]
1456 fn config_unknown_param_ignored() {
1457 let cfg = Config::from_url(
1458 "postgres://user:pass@localhost/db?application_name=myapp&connect_timeout=10",
1459 )
1460 .unwrap();
1461 assert_eq!(cfg.user, "user");
1463 assert_eq!(cfg.host, "localhost");
1464 assert_eq!(cfg.database, "db");
1465 assert_eq!(cfg.statement_timeout_secs, 30);
1467 assert_eq!(cfg.ssl, SslMode::Prefer);
1468 }
1469
1470 #[test]
1472 fn url_decode_double_percent_encoding() {
1473 assert_eq!(url_decode("%2525").unwrap(), "%25");
1475 }
1476
1477 #[test]
1479 fn config_explicit_empty_password() {
1480 let cfg = Config::from_url("postgres://user:@localhost/db").unwrap();
1481 assert_eq!(cfg.user, "user");
1482 assert_eq!(cfg.password, "");
1483 }
1484
1485 #[test]
1487 fn config_special_chars_in_user() {
1488 let cfg = Config::from_url("postgres://my%2Fuser:pass@localhost/my%2Fdb").unwrap();
1489 assert_eq!(cfg.user, "my/user");
1490 assert_eq!(cfg.database, "my/db");
1491 }
1492
1493 #[test]
1495 fn url_decode_plus_is_literal() {
1496 assert_eq!(url_decode("a+b").unwrap(), "a+b");
1497 }
1498
1499 #[test]
1501 fn config_minimal_valid_url() {
1502 let cfg = Config::from_url("postgres://user@localhost/db").unwrap();
1503 assert_eq!(cfg.user, "user");
1504 assert_eq!(cfg.password, "");
1505 assert_eq!(cfg.host, "localhost");
1506 assert_eq!(cfg.port, 5432);
1507 assert_eq!(cfg.database, "db");
1508 }
1509
1510 #[test]
1512 fn config_empty_param_segments() {
1513 let cfg =
1514 Config::from_url("postgres://user:pass@localhost/db?&&statement_timeout=60&&").unwrap();
1515 assert_eq!(cfg.statement_timeout_secs, 60);
1516 }
1517
1518 #[test]
1523 fn hash_sql_deterministic() {
1524 let h1 = hash_sql("SELECT 1");
1525 let h2 = hash_sql("SELECT 1");
1526 assert_eq!(h1, h2);
1527 }
1528
1529 #[test]
1530 fn hash_sql_different_queries() {
1531 let h1 = hash_sql("SELECT 1");
1532 let h2 = hash_sql("SELECT 2");
1533 assert_ne!(h1, h2);
1534 }
1535
1536 #[test]
1537 fn hash_sql_empty() {
1538 let _h = hash_sql(""); }
1540
1541 #[test]
1542 fn hash_sql_whitespace_only() {
1543 let h = hash_sql(" ");
1544 assert_ne!(h, hash_sql(""));
1545 }
1546
1547 #[test]
1548 fn hash_sql_very_long() {
1549 let long_sql = "SELECT ".to_string() + &"x".repeat(10_000);
1550 let h = hash_sql(&long_sql);
1551 assert_eq!(h, hash_sql(&long_sql));
1552 }
1553
1554 #[test]
1555 fn hash_sql_unicode() {
1556 let h = hash_sql("SELECT '\u{1F600}'");
1557 assert_ne!(h, hash_sql("SELECT 'x'"));
1558 }
1559
1560 #[test]
1565 fn notification_struct_fields() {
1566 let n = Notification {
1567 pid: 42,
1568 channel: "test_chan".to_owned(),
1569 payload: "hello".to_owned(),
1570 };
1571 assert_eq!(n.pid, 42);
1572 assert_eq!(n.channel, "test_chan");
1573 assert_eq!(n.payload, "hello");
1574 }
1575
1576 #[test]
1577 fn notification_clone() {
1578 let n = Notification {
1579 pid: 1,
1580 channel: "c".to_owned(),
1581 payload: "p".to_owned(),
1582 };
1583 let n2 = n.clone();
1584 assert_eq!(n2.pid, 1);
1585 assert_eq!(n2.channel, "c");
1586 }
1587
1588 #[test]
1589 fn notification_debug() {
1590 let n = Notification {
1591 pid: 1,
1592 channel: "c".to_owned(),
1593 payload: "p".to_owned(),
1594 };
1595 let dbg = format!("{n:?}");
1596 assert!(dbg.contains("Notification"));
1597 }
1598
1599 #[test]
1604 fn query_result_empty() {
1605 let result = QueryResult {
1606 all_col_offsets: vec![],
1607 num_cols: 0,
1608 columns: Arc::from(Vec::new()),
1609 affected_rows: 0,
1610 data_buf: None,
1611 };
1612 assert!(result.is_empty());
1613 assert_eq!(result.len(), 0);
1614 }
1615
1616 #[test]
1617 fn query_result_from_parts() {
1618 let result = QueryResult::from_parts(vec![(0, 4), (0, -1)], 2, Arc::from(Vec::new()), 5);
1619 assert_eq!(result.len(), 1);
1620 assert_eq!(result.num_cols, 2);
1621 assert_eq!(result.affected_rows, 5);
1622 }
1623
1624 #[test]
1625 fn query_result_affected_rows() {
1626 let result = QueryResult {
1627 all_col_offsets: vec![],
1628 num_cols: 0,
1629 columns: Arc::from(Vec::new()),
1630 affected_rows: 42,
1631 data_buf: None,
1632 };
1633 assert_eq!(result.affected_rows, 42);
1634 assert!(result.is_empty());
1635 }
1636
1637 fn make_data_row(columns: &[Option<&[u8]>]) -> Vec<u8> {
1644 let mut buf = Vec::new();
1645 buf.extend_from_slice(&(columns.len() as i16).to_be_bytes());
1646 for col in columns {
1647 match col {
1648 Some(data) => {
1649 buf.extend_from_slice(&(data.len() as i32).to_be_bytes());
1650 buf.extend_from_slice(data);
1651 }
1652 None => {
1653 buf.extend_from_slice(&(-1i32).to_be_bytes());
1654 }
1655 }
1656 }
1657 buf
1658 }
1659
1660 #[test]
1661 fn pg_data_row_get_i32() {
1662 let data = make_data_row(&[Some(&42i32.to_be_bytes())]);
1663 let row = PgDataRow::new(&data).unwrap();
1664 assert_eq!(row.get_i32(0), Some(42));
1665 assert_eq!(row.column_count(), 1);
1666 }
1667
1668 #[test]
1669 fn pg_data_row_get_i64() {
1670 let data = make_data_row(&[Some(&12345i64.to_be_bytes())]);
1671 let row = PgDataRow::new(&data).unwrap();
1672 assert_eq!(row.get_i64(0), Some(12345));
1673 }
1674
1675 #[test]
1676 fn pg_data_row_get_str() {
1677 let data = make_data_row(&[Some(b"hello")]);
1678 let row = PgDataRow::new(&data).unwrap();
1679 assert_eq!(row.get_str(0), Some("hello"));
1680 }
1681
1682 #[test]
1683 fn pg_data_row_get_bytes() {
1684 let data = make_data_row(&[Some(&[0xDE, 0xAD, 0xBE, 0xEF])]);
1685 let row = PgDataRow::new(&data).unwrap();
1686 assert_eq!(row.get_bytes(0), Some(&[0xDE, 0xAD, 0xBE, 0xEF][..]));
1687 }
1688
1689 #[test]
1690 fn pg_data_row_get_bool() {
1691 let data = make_data_row(&[Some(&[1u8])]);
1692 let row = PgDataRow::new(&data).unwrap();
1693 assert_eq!(row.get_bool(0), Some(true));
1694
1695 let data = make_data_row(&[Some(&[0u8])]);
1696 let row = PgDataRow::new(&data).unwrap();
1697 assert_eq!(row.get_bool(0), Some(false));
1698 }
1699
1700 #[test]
1701 fn pg_data_row_get_f64() {
1702 let data = make_data_row(&[Some(&3.14f64.to_be_bytes())]);
1703 let row = PgDataRow::new(&data).unwrap();
1704 assert!((row.get_f64(0).unwrap() - 3.14).abs() < 1e-10);
1705 }
1706
1707 #[test]
1708 fn pg_data_row_null_column() {
1709 let data = make_data_row(&[None]);
1710 let row = PgDataRow::new(&data).unwrap();
1711 assert!(row.is_null(0));
1712 assert_eq!(row.get_i32(0), None);
1713 assert_eq!(row.get_str(0), None);
1714 }
1715
1716 #[test]
1717 fn pg_data_row_multiple_columns() {
1718 let data = make_data_row(&[
1719 Some(&42i32.to_be_bytes()),
1720 Some(b"alice"),
1721 Some(b"alice@example.com"),
1722 Some(&[1u8]),
1723 Some(&3.14f64.to_be_bytes()),
1724 ]);
1725 let row = PgDataRow::new(&data).unwrap();
1726 assert_eq!(row.column_count(), 5);
1727 assert_eq!(row.get_i32(0), Some(42));
1728 assert_eq!(row.get_str(1), Some("alice"));
1729 assert_eq!(row.get_str(2), Some("alice@example.com"));
1730 assert_eq!(row.get_bool(3), Some(true));
1731 assert!((row.get_f64(4).unwrap() - 3.14).abs() < 1e-10);
1732 }
1733
1734 #[test]
1735 fn pg_data_row_mixed_null() {
1736 let data = make_data_row(&[Some(&42i32.to_be_bytes()), None, Some(b"text")]);
1737 let row = PgDataRow::new(&data).unwrap();
1738 assert_eq!(row.get_i32(0), Some(42));
1739 assert!(row.is_null(1));
1740 assert_eq!(row.get_str(1), None);
1741 assert_eq!(row.get_str(2), Some("text"));
1742 }
1743
1744 #[test]
1745 fn pg_data_row_empty() {
1746 let data = make_data_row(&[]);
1747 let row = PgDataRow::new(&data).unwrap();
1748 assert_eq!(row.column_count(), 0);
1749 }
1750
1751 #[test]
1752 fn pg_data_row_too_short() {
1753 let data = vec![0u8]; assert!(PgDataRow::new(&data).is_err());
1755 }
1756
1757 #[test]
1758 fn pg_data_row_truncated() {
1759 let mut data = Vec::new();
1761 data.extend_from_slice(&2i16.to_be_bytes());
1762 data.extend_from_slice(&4i32.to_be_bytes());
1763 data.extend_from_slice(&42i32.to_be_bytes());
1764 assert!(PgDataRow::new(&data).is_err());
1766 }
1767
1768 #[test]
1769 fn pg_data_row_get_i16() {
1770 let data = make_data_row(&[Some(&7i16.to_be_bytes())]);
1771 let row = PgDataRow::new(&data).unwrap();
1772 assert_eq!(row.get_i16(0), Some(7));
1773 }
1774
1775 #[test]
1776 fn pg_data_row_get_f32() {
1777 let data = make_data_row(&[Some(&2.5f32.to_be_bytes())]);
1778 let row = PgDataRow::new(&data).unwrap();
1779 assert!((row.get_f32(0).unwrap() - 2.5).abs() < 1e-6);
1780 }
1781
1782 #[test]
1783 fn pg_data_row_get_raw_null() {
1784 let data = make_data_row(&[None]);
1785 let row = PgDataRow::new(&data).unwrap();
1786 assert_eq!(row.get_raw(0), None);
1787 }
1788
1789 #[test]
1790 fn pg_data_row_get_raw_data() {
1791 let data = make_data_row(&[Some(&[1, 2, 3])]);
1792 let row = PgDataRow::new(&data).unwrap();
1793 assert_eq!(row.get_raw(0), Some(&[1u8, 2, 3][..]));
1794 }
1795
1796 #[test]
1797 fn pg_data_row_stack_alloc_16_columns() {
1798 let cols: Vec<Option<&[u8]>> = (0..16).map(|_| Some(&[0u8][..])).collect();
1800 let data = make_data_row(&cols);
1801 let row = PgDataRow::new(&data).unwrap();
1802 assert_eq!(row.column_count(), 16);
1803 for i in 0..16 {
1805 assert_eq!(row.get_raw(i), Some(&[0u8][..]));
1806 }
1807 }
1808
1809 #[test]
1814 fn inline_sequential_decode_five_columns() {
1815 let data = make_data_row(&[
1816 Some(&42i32.to_be_bytes()),
1817 Some(b"alice"),
1818 Some(b"alice@example.com"),
1819 Some(&[1u8]),
1820 Some(&3.14f64.to_be_bytes()),
1821 ]);
1822
1823 let mut pos: usize = 2; let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1828 pos += 4;
1829 assert_eq!(len, 4);
1830 let id = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1831 pos += len as usize;
1832 assert_eq!(id, 42);
1833
1834 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1836 pos += 4;
1837 assert_eq!(len, 5);
1838 let name = std::str::from_utf8(&data[pos..pos + len as usize]).unwrap();
1839 pos += len as usize;
1840 assert_eq!(name, "alice");
1841
1842 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1844 pos += 4;
1845 let email = std::str::from_utf8(&data[pos..pos + len as usize]).unwrap();
1846 pos += len as usize;
1847 assert_eq!(email, "alice@example.com");
1848
1849 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1851 pos += 4;
1852 assert_eq!(len, 1);
1853 let active = data[pos] != 0;
1854 pos += len as usize;
1855 assert!(active);
1856
1857 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1859 pos += 4;
1860 assert_eq!(len, 8);
1861 let score = f64::from_be_bytes([
1862 data[pos],
1863 data[pos + 1],
1864 data[pos + 2],
1865 data[pos + 3],
1866 data[pos + 4],
1867 data[pos + 5],
1868 data[pos + 6],
1869 data[pos + 7],
1870 ]);
1871 pos += len as usize;
1872 assert!((score - 3.14).abs() < 1e-10);
1873 assert_eq!(pos, data.len());
1874 }
1875
1876 #[test]
1878 fn inline_sequential_decode_with_nulls() {
1879 let data = make_data_row(&[
1880 Some(&42i32.to_be_bytes()),
1881 None, Some(b"text"),
1883 ]);
1884
1885 let mut pos: usize = 2;
1886
1887 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1889 pos += 4;
1890 let id = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1891 pos += len as usize;
1892 assert_eq!(id, 42);
1893
1894 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1896 pos += 4;
1897 let name: Option<&str> = if len < 0 {
1898 None
1899 } else {
1900 let s = std::str::from_utf8(&data[pos..pos + len as usize]).unwrap();
1901 pos += len as usize;
1902 Some(s)
1903 };
1904 assert!(name.is_none());
1905
1906 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1908 pos += 4;
1909 let txt = std::str::from_utf8(&data[pos..pos + len as usize]).unwrap();
1910 pos += len as usize;
1911 assert_eq!(txt, "text");
1912 assert_eq!(pos, data.len());
1913 }
1914
1915 #[test]
1917 fn inline_sequential_decode_all_scalar_types() {
1918 let data = make_data_row(&[
1919 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()), ]);
1926
1927 let mut pos: usize = 2;
1928
1929 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1931 pos += 4;
1932 let v_bool = data[pos] != 0;
1933 pos += len as usize;
1934 assert!(v_bool);
1935
1936 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1938 pos += 4;
1939 let v_i16 = i16::from_be_bytes([data[pos], data[pos + 1]]);
1940 pos += len as usize;
1941 assert_eq!(v_i16, 7);
1942
1943 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1945 pos += 4;
1946 let v_i32 = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1947 pos += len as usize;
1948 assert_eq!(v_i32, 42);
1949
1950 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1952 pos += 4;
1953 let v_i64 = i64::from_be_bytes([
1954 data[pos],
1955 data[pos + 1],
1956 data[pos + 2],
1957 data[pos + 3],
1958 data[pos + 4],
1959 data[pos + 5],
1960 data[pos + 6],
1961 data[pos + 7],
1962 ]);
1963 pos += len as usize;
1964 assert_eq!(v_i64, 12345);
1965
1966 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1968 pos += 4;
1969 let v_f32 = f32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1970 pos += len as usize;
1971 assert!((v_f32 - 2.5).abs() < 1e-6);
1972
1973 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1975 pos += 4;
1976 let v_f64 = f64::from_be_bytes([
1977 data[pos],
1978 data[pos + 1],
1979 data[pos + 2],
1980 data[pos + 3],
1981 data[pos + 4],
1982 data[pos + 5],
1983 data[pos + 6],
1984 data[pos + 7],
1985 ]);
1986 pos += len as usize;
1987 assert!((v_f64 - 3.14).abs() < 1e-10);
1988 assert_eq!(pos, data.len());
1989 }
1990
1991 #[test]
1993 fn pg_data_row_new_is_public() {
1994 let data = make_data_row(&[Some(&42i32.to_be_bytes())]);
1995 let row = PgDataRow::new(&data).unwrap();
1997 assert_eq!(row.get_i32(0), Some(42));
1998 }
1999
2000 #[test]
2002 fn inline_decode_matches_pgdatarow() {
2003 let data = make_data_row(&[
2004 Some(&99i32.to_be_bytes()),
2005 Some(b"hello world"),
2006 None,
2007 Some(&[0u8]),
2008 Some(&1.23f64.to_be_bytes()),
2009 ]);
2010
2011 let row = PgDataRow::new(&data).unwrap();
2013 let dr_i32 = row.get_i32(0);
2014 let dr_str = row.get_str(1);
2015 let dr_null = row.get_str(2);
2016 let dr_bool = row.get_bool(3);
2017 let dr_f64 = row.get_f64(4);
2018
2019 let mut pos: usize = 2;
2021
2022 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
2023 pos += 4;
2024 let in_i32 = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
2025 pos += len as usize;
2026
2027 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
2028 pos += 4;
2029 let in_str = std::str::from_utf8(&data[pos..pos + len as usize]).unwrap();
2030 pos += len as usize;
2031
2032 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
2033 pos += 4;
2034 let in_null: Option<&str> = if len < 0 { None } else { unreachable!() };
2035
2036 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
2037 pos += 4;
2038 let in_bool = data[pos] != 0;
2039 pos += len as usize;
2040
2041 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
2042 pos += 4;
2043 let in_f64 = f64::from_be_bytes([
2044 data[pos],
2045 data[pos + 1],
2046 data[pos + 2],
2047 data[pos + 3],
2048 data[pos + 4],
2049 data[pos + 5],
2050 data[pos + 6],
2051 data[pos + 7],
2052 ]);
2053 pos += len as usize;
2054
2055 assert_eq!(dr_i32, Some(in_i32));
2057 assert_eq!(dr_str, Some(in_str));
2058 assert_eq!(dr_null, in_null);
2059 assert_eq!(dr_bool, Some(in_bool));
2060 assert!((dr_f64.unwrap() - in_f64).abs() < 1e-15);
2061 assert_eq!(pos, data.len());
2062 }
2063
2064 #[test]
2069 fn pg_data_row_all_null_columns() {
2070 let data = make_data_row(&[None, None, None, None, None]);
2071 let row = PgDataRow::new(&data).unwrap();
2072 assert_eq!(row.column_count(), 5);
2073 for i in 0..5 {
2074 assert!(row.is_null(i), "column {i} should be null");
2075 assert_eq!(row.get_raw(i), None);
2076 assert_eq!(row.get_i32(i), None);
2077 assert_eq!(row.get_i64(i), None);
2078 assert_eq!(row.get_str(i), None);
2079 assert_eq!(row.get_bool(i), None);
2080 assert_eq!(row.get_f64(i), None);
2081 }
2082 }
2083
2084 #[test]
2085 fn pg_data_row_very_long_text() {
2086 let long_text = "x".repeat(2048);
2087 let data = make_data_row(&[Some(long_text.as_bytes())]);
2088 let row = PgDataRow::new(&data).unwrap();
2089 assert_eq!(row.get_str(0), Some(long_text.as_str()));
2090 }
2091
2092 #[test]
2093 fn pg_data_row_empty_text() {
2094 let data = make_data_row(&[Some(b"")]);
2095 let row = PgDataRow::new(&data).unwrap();
2096 assert!(!row.is_null(0));
2097 assert_eq!(row.get_str(0), Some(""));
2098 assert_eq!(row.get_bytes(0), Some(&[][..]));
2099 }
2100
2101 #[test]
2102 fn pg_data_row_20_columns_exceeds_inline() {
2103 let col_data: Vec<[u8; 4]> = (0..20).map(|i: i32| i.to_be_bytes()).collect();
2104 let cols: Vec<Option<&[u8]>> = col_data.iter().map(|b| Some(b.as_slice())).collect();
2105 let data = make_data_row(&cols);
2106 let row = PgDataRow::new(&data).unwrap();
2107 assert_eq!(row.column_count(), 20);
2108 for i in 0..20 {
2109 assert_eq!(row.get_i32(i), Some(i as i32));
2110 }
2111 }
2112
2113 #[test]
2114 fn pg_data_row_is_null_each_position() {
2115 let data = make_data_row(&[Some(&1i32.to_be_bytes()), None, Some(&3i32.to_be_bytes())]);
2117 let row = PgDataRow::new(&data).unwrap();
2118 assert!(!row.is_null(0));
2119 assert!(row.is_null(1));
2120 assert!(!row.is_null(2));
2121 }
2122
2123 #[test]
2124 fn pg_data_row_negative_column_count() {
2125 let data = (-1i16).to_be_bytes();
2126 assert!(PgDataRow::new(&data).is_err());
2127 }
2128
2129 #[test]
2130 fn pg_data_row_get_str_invalid_utf8() {
2131 let invalid_utf8 = &[0xFF, 0xFE, 0x80];
2132 let data = make_data_row(&[Some(invalid_utf8)]);
2133 let row = PgDataRow::new(&data).unwrap();
2134 assert_eq!(row.get_str(0), None);
2136 assert_eq!(row.get_bytes(0), Some(&[0xFF, 0xFE, 0x80][..]));
2137 }
2138
2139 #[test]
2140 fn pg_data_row_get_i32_wrong_length() {
2141 let data = make_data_row(&[Some(&7i16.to_be_bytes())]);
2143 let row = PgDataRow::new(&data).unwrap();
2144 assert_eq!(row.get_i32(0), None); assert_eq!(row.get_i16(0), Some(7)); }
2147
2148 #[test]
2149 fn pg_data_row_get_i64_wrong_length() {
2150 let data = make_data_row(&[Some(&42i32.to_be_bytes())]);
2152 let row = PgDataRow::new(&data).unwrap();
2153 assert_eq!(row.get_i64(0), None);
2154 }
2155
2156 #[test]
2157 fn pg_data_row_get_f64_wrong_length() {
2158 let data = make_data_row(&[Some(&2.5f32.to_be_bytes())]);
2159 let row = PgDataRow::new(&data).unwrap();
2160 assert_eq!(row.get_f64(0), None); }
2162
2163 #[test]
2164 fn pg_data_row_get_f32_wrong_length() {
2165 let data = make_data_row(&[Some(&3.14f64.to_be_bytes())]);
2166 let row = PgDataRow::new(&data).unwrap();
2167 assert_eq!(row.get_f32(0), None); }
2169
2170 #[test]
2171 fn pg_data_row_get_bool_wrong_length() {
2172 let data = make_data_row(&[Some(&42i32.to_be_bytes())]);
2174 let row = PgDataRow::new(&data).unwrap();
2175 assert_eq!(row.get_bool(0), None);
2176 }
2177
2178 #[test]
2179 fn pg_data_row_unicode_text() {
2180 let texts = [
2181 "\u{1F600}\u{1F4A9}\u{1F680}", "\u{4e16}\u{754c}", "\u{0645}\u{0631}\u{062D}", "\u{1F468}\u{200D}\u{1F469}", ];
2186 for text in &texts {
2187 let data = make_data_row(&[Some(text.as_bytes())]);
2188 let row = PgDataRow::new(&data).unwrap();
2189 assert_eq!(row.get_str(0), Some(*text));
2190 }
2191 }
2192
2193 #[test]
2194 fn pg_data_row_i32_boundary_values() {
2195 for &val in &[i32::MIN, -1, 0, 1, i32::MAX] {
2196 let data = make_data_row(&[Some(&val.to_be_bytes())]);
2197 let row = PgDataRow::new(&data).unwrap();
2198 assert_eq!(row.get_i32(0), Some(val), "failed for {val}");
2199 }
2200 }
2201
2202 #[test]
2203 fn pg_data_row_i64_boundary_values() {
2204 for &val in &[i64::MIN, -1, 0, 1, i64::MAX] {
2205 let data = make_data_row(&[Some(&val.to_be_bytes())]);
2206 let row = PgDataRow::new(&data).unwrap();
2207 assert_eq!(row.get_i64(0), Some(val), "failed for {val}");
2208 }
2209 }
2210
2211 #[test]
2212 fn pg_data_row_f64_special_values() {
2213 let data = make_data_row(&[Some(&f64::INFINITY.to_be_bytes())]);
2214 let row = PgDataRow::new(&data).unwrap();
2215 assert_eq!(row.get_f64(0), Some(f64::INFINITY));
2216
2217 let data = make_data_row(&[Some(&f64::NEG_INFINITY.to_be_bytes())]);
2218 let row = PgDataRow::new(&data).unwrap();
2219 assert_eq!(row.get_f64(0), Some(f64::NEG_INFINITY));
2220
2221 let data = make_data_row(&[Some(&f64::NAN.to_be_bytes())]);
2222 let row = PgDataRow::new(&data).unwrap();
2223 assert!(row.get_f64(0).unwrap().is_nan());
2224 }
2225
2226 #[test]
2227 fn pg_data_row_f32_special_values() {
2228 let data = make_data_row(&[Some(&f32::INFINITY.to_be_bytes())]);
2229 let row = PgDataRow::new(&data).unwrap();
2230 assert_eq!(row.get_f32(0), Some(f32::INFINITY));
2231
2232 let data = make_data_row(&[Some(&f32::NAN.to_be_bytes())]);
2233 let row = PgDataRow::new(&data).unwrap();
2234 assert!(row.get_f32(0).unwrap().is_nan());
2235 }
2236
2237 #[test]
2238 fn pg_data_row_i16_boundary_values() {
2239 for &val in &[i16::MIN, -1, 0, 1, i16::MAX] {
2240 let data = make_data_row(&[Some(&val.to_be_bytes())]);
2241 let row = PgDataRow::new(&data).unwrap();
2242 assert_eq!(row.get_i16(0), Some(val));
2243 }
2244 }
2245
2246 mod proptest_fuzz {
2247 use super::*;
2248 use proptest::prelude::*;
2249
2250 proptest! {
2251 #[test]
2252 fn config_from_url_never_panics(url in ".*") {
2253 let _ = Config::from_url(&url);
2254 }
2255
2256 #[test]
2257 fn url_decode_never_panics(s in ".*") {
2258 let _ = url_decode(&s);
2259 }
2260
2261 #[test]
2262 fn pg_data_row_new_never_panics(data in proptest::collection::vec(any::<u8>(), 0..8192)) {
2263 let _ = PgDataRow::new(&data);
2264 }
2265
2266 #[test]
2267 fn hash_sql_never_panics(sql in ".*") {
2268 let _ = hash_sql(&sql);
2269 }
2270 }
2271 }
2272}