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 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
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum SslMode {
58 Disable,
60 Prefer,
62 Require,
64}
65
66#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
71pub enum StatementCacheMode {
72 #[default]
77 Named,
78 Disabled,
84}
85
86impl Config {
87 pub fn from_url(url: &str) -> Result<Self, DriverError> {
124 let url = url
125 .strip_prefix("postgres://")
126 .or_else(|| url.strip_prefix("postgresql://"))
127 .ok_or_else(|| DriverError::Protocol("URL must start with postgres://".into()))?;
128
129 let (userinfo, rest) = url
131 .split_once('@')
132 .ok_or_else(|| DriverError::Protocol("missing @ in connection URL".into()))?;
133
134 let (user, password) = userinfo.split_once(':').unwrap_or((userinfo, ""));
135
136 let (hostport, rest) = rest.split_once('/').unwrap_or((rest, ""));
138 let (database, params) = rest.split_once('?').unwrap_or((rest, ""));
139
140 let (host, port) = if let Some((h, p)) = hostport.split_once(':') {
141 let port = p
142 .parse::<u16>()
143 .map_err(|_| DriverError::Protocol(format!("invalid port: {p}")))?;
144 (h.to_owned(), port)
145 } else {
146 (hostport.to_owned(), 5432)
147 };
148
149 let mut ssl = SslMode::Prefer;
150 let mut statement_timeout_secs: u32 = 30;
151 let mut statement_cache_mode = StatementCacheMode::Named;
152 let mut host_override: Option<String> = None;
153 let mut ssl_root_cert: Option<String> = None;
154 let mut ssl_cert: Option<String> = None;
155 let mut ssl_key: Option<String> = None;
156 for param in params.split('&') {
157 if param.is_empty() {
158 continue;
159 }
160 if let Some(val) = param.strip_prefix("sslmode=") {
161 ssl = match val {
163 "disable" => SslMode::Disable,
164 "prefer" => SslMode::Prefer,
165 "require" => SslMode::Require,
166 _ => {
167 return Err(DriverError::Protocol(format!(
168 "unknown sslmode: '{val}' (expected: disable, prefer, require)"
169 )));
170 }
171 };
172 } else if let Some(val) = param.strip_prefix("statement_timeout=") {
173 statement_timeout_secs = val.parse::<u32>().unwrap_or(30);
174 } else if let Some(val) = param.strip_prefix("statement_cache=") {
175 statement_cache_mode = match val {
176 "named" => StatementCacheMode::Named,
177 "disabled" => StatementCacheMode::Disabled,
178 _ => {
179 return Err(DriverError::Protocol(format!(
180 "unknown statement_cache mode: '{val}' (expected: named, disabled)"
181 )));
182 }
183 };
184 } else if let Some(val) = param.strip_prefix("host=") {
185 host_override = Some(url_decode(val)?);
186 } else if let Some(val) = param.strip_prefix("sslrootcert=") {
187 ssl_root_cert = Some(url_decode(val)?);
188 } else if let Some(val) = param.strip_prefix("sslcert=") {
189 ssl_cert = Some(url_decode(val)?);
190 } else if let Some(val) = param.strip_prefix("sslkey=") {
191 ssl_key = Some(url_decode(val)?);
192 }
193 }
194
195 let final_host = if let Some(h) = host_override {
198 h
199 } else {
200 url_decode(&host)?
201 };
202
203 let config = Config {
204 host: final_host,
205 port,
206 user: url_decode(user)?,
207 password: url_decode(password)?,
208 database: if database.is_empty() {
209 url_decode(user)?
210 } else {
211 url_decode(database)?
212 },
213 ssl,
214 statement_timeout_secs,
215 statement_cache_mode,
216 ssl_root_cert,
217 ssl_cert,
218 ssl_key,
219 };
220 config.validate()?;
221 Ok(config)
222 }
223
224 pub fn validate(&self) -> Result<(), DriverError> {
229 if self.host.is_empty() {
230 return Err(DriverError::Protocol("host cannot be empty".into()));
231 }
232 if self.user.is_empty() {
233 return Err(DriverError::Protocol("user cannot be empty".into()));
234 }
235 if self.database.is_empty() {
236 return Err(DriverError::Protocol("database cannot be empty".into()));
237 }
238 Ok(())
239 }
240
241 pub fn host_is_uds(&self) -> bool {
246 self.host.starts_with('/')
247 }
248
249 pub fn uds_path(&self) -> String {
253 format!("{}/.s.PGSQL.{}", self.host, self.port)
254 }
255}
256
257fn url_decode(s: &str) -> Result<String, DriverError> {
267 let mut bytes = Vec::with_capacity(s.len());
268 let input = s.as_bytes();
269 let mut i = 0;
270 while i < input.len() {
271 if input[i] == b'%' {
272 if i + 2 >= input.len() {
273 return Err(DriverError::Protocol(format!(
274 "malformed percent-encoding in URL: '{s}'"
275 )));
276 }
277 let hi = hex_val(input[i + 1]).ok_or_else(|| {
278 DriverError::Protocol(format!(
279 "invalid hex digit '{}' in URL: '{s}'",
280 input[i + 1] as char
281 ))
282 })?;
283 let lo = hex_val(input[i + 2]).ok_or_else(|| {
284 DriverError::Protocol(format!(
285 "invalid hex digit '{}' in URL: '{s}'",
286 input[i + 2] as char
287 ))
288 })?;
289 bytes.push(hi * 16 + lo);
290 i += 3;
291 } else {
292 bytes.push(input[i]);
293 i += 1;
294 }
295 }
296 String::from_utf8(bytes)
297 .map_err(|_| DriverError::Protocol(format!("invalid UTF-8 in URL: '{s}'")))
298}
299
300fn hex_val(b: u8) -> Option<u8> {
301 match b {
302 b'0'..=b'9' => Some(b - b'0'),
303 b'a'..=b'f' => Some(b - b'a' + 10),
304 b'A'..=b'F' => Some(b - b'A' + 10),
305 _ => None,
306 }
307}
308
309pub(crate) enum StartupAction {
315 AuthOk,
316 AuthCleartext,
317 AuthMd5([u8; 4]),
318 AuthSasl(Vec<u8>),
319 ParameterStatus(Box<str>, Box<str>),
320 BackendKeyData(i32, i32),
321 ReadyForQuery(u8),
322 Error(String),
323 Notice,
324}
325
326#[derive(Debug, Clone)]
332pub struct ColumnDesc {
333 pub name: Box<str>,
335 pub type_oid: u32,
337 pub table_oid: u32,
339 pub type_size: i16,
341 pub column_id: i16,
343}
344
345#[derive(Debug, Clone)]
348pub struct PrepareResult {
349 pub columns: Vec<ColumnDesc>,
351 pub param_oids: Vec<u32>,
353}
354
355pub type SimpleRow = Vec<Option<String>>;
360
361#[derive(Debug, Clone)]
367pub struct Notification {
368 pub pid: i32,
370 pub channel: String,
372 pub payload: String,
374}
375
376pub struct QueryResult {
393 pub(crate) all_col_offsets: Vec<(usize, i32)>,
397 pub(crate) num_cols: usize,
399 pub(crate) columns: Arc<[ColumnDesc]>,
400 pub(crate) affected_rows: u64,
401 pub(crate) data_buf: Option<Vec<u8>>,
405}
406
407impl QueryResult {
408 pub fn from_parts(
412 all_col_offsets: Vec<(usize, i32)>,
413 num_cols: usize,
414 columns: Arc<[ColumnDesc]>,
415 affected_rows: u64,
416 ) -> Self {
417 Self {
418 all_col_offsets,
419 num_cols,
420 columns,
421 affected_rows,
422 data_buf: None,
423 }
424 }
425
426 pub fn from_parts_with_buf(
428 all_col_offsets: Vec<(usize, i32)>,
429 num_cols: usize,
430 columns: Arc<[ColumnDesc]>,
431 affected_rows: u64,
432 data_buf: Vec<u8>,
433 ) -> Self {
434 Self {
435 all_col_offsets,
436 num_cols,
437 columns,
438 affected_rows,
439 data_buf: if data_buf.is_empty() {
440 None
441 } else {
442 Some(data_buf)
443 },
444 }
445 }
446
447 pub fn len(&self) -> usize {
449 if self.num_cols == 0 {
450 return 0;
451 }
452 self.all_col_offsets.len() / self.num_cols
453 }
454
455 pub fn is_empty(&self) -> bool {
457 self.all_col_offsets.is_empty()
458 }
459
460 pub fn affected_rows(&self) -> u64 {
462 self.affected_rows
463 }
464
465 pub fn columns(&self) -> &[ColumnDesc] {
467 &self.columns
468 }
469
470 pub fn row<'a>(&'a self, idx: usize, arena: &'a Arena) -> Row<'a> {
473 let start = idx * self.num_cols;
474 let end = start + self.num_cols;
475 Row {
476 data: self.data_buf.as_deref(),
477 arena,
478 col_offsets: &self.all_col_offsets[start..end],
479 columns: &self.columns,
480 }
481 }
482
483 pub fn take_col_offsets(&mut self) -> Vec<(usize, i32)> {
488 std::mem::take(&mut self.all_col_offsets)
489 }
490
491 pub fn take_data_buf(&mut self) -> Option<Vec<u8>> {
493 self.data_buf.take()
494 }
495
496 pub fn rows<'a>(&'a self, arena: &'a Arena) -> impl Iterator<Item = Row<'a>> {
498 let num_cols = self.num_cols;
499 let columns = &self.columns;
500 let data = self.data_buf.as_deref();
501 self.all_col_offsets
502 .chunks(num_cols.max(1))
503 .map(move |chunk| Row {
504 data,
505 arena,
506 col_offsets: chunk,
507 columns,
508 })
509 }
510}
511
512pub struct Row<'a> {
523 data: Option<&'a [u8]>,
526 arena: &'a Arena,
527 col_offsets: &'a [(usize, i32)],
528 columns: &'a [ColumnDesc],
529}
530
531impl<'a> Row<'a> {
532 #[inline]
534 pub fn get_raw(&self, idx: usize) -> Option<&'a [u8]> {
535 let (offset, len) = self.col_offsets[idx];
536 if len < 0 {
537 None
538 } else if let Some(buf) = self.data {
539 Some(&buf[offset..offset + len as usize])
540 } else {
541 Some(self.arena.get(offset, len as usize))
542 }
543 }
544
545 #[inline]
547 pub fn is_null(&self, idx: usize) -> bool {
548 self.col_offsets[idx].1 < 0
549 }
550
551 #[inline]
553 pub fn column_count(&self) -> usize {
554 self.col_offsets.len()
555 }
556
557 #[inline]
559 pub fn get_bool(&self, idx: usize) -> Option<bool> {
560 self.get_raw(idx)
561 .and_then(|data| crate::codec::decode_bool(data).ok())
562 }
563
564 #[inline]
566 pub fn get_i16(&self, idx: usize) -> Option<i16> {
567 self.get_raw(idx)
568 .and_then(|data| crate::codec::decode_i16(data).ok())
569 }
570
571 #[inline]
573 pub fn get_i32(&self, idx: usize) -> Option<i32> {
574 self.get_raw(idx)
575 .and_then(|data| crate::codec::decode_i32(data).ok())
576 }
577
578 #[inline]
580 pub fn get_i64(&self, idx: usize) -> Option<i64> {
581 self.get_raw(idx)
582 .and_then(|data| crate::codec::decode_i64(data).ok())
583 }
584
585 #[inline]
587 pub fn get_f32(&self, idx: usize) -> Option<f32> {
588 self.get_raw(idx)
589 .and_then(|data| crate::codec::decode_f32(data).ok())
590 }
591
592 #[inline]
594 pub fn get_f64(&self, idx: usize) -> Option<f64> {
595 self.get_raw(idx)
596 .and_then(|data| crate::codec::decode_f64(data).ok())
597 }
598
599 #[inline]
601 pub fn get_str(&self, idx: usize) -> Option<&'a str> {
602 self.get_raw(idx)
603 .and_then(|data| crate::codec::decode_str(data).ok())
604 }
605
606 #[inline]
608 pub fn get_bytes(&self, idx: usize) -> Option<&'a [u8]> {
609 self.get_raw(idx)
610 }
611
612 #[inline]
614 pub fn column_name(&self, idx: usize) -> &str {
615 &self.columns[idx].name
616 }
617
618 #[inline]
620 pub fn column_type_oid(&self, idx: usize) -> u32 {
621 self.columns[idx].type_oid
622 }
623}
624
625pub struct PgDataRow<'a> {
638 data: &'a [u8],
639 offsets: smallvec::SmallVec<[(usize, i32); 16]>,
642}
643
644impl<'a> PgDataRow<'a> {
645 pub fn new(data: &'a [u8]) -> Result<Self, DriverError> {
650 if data.len() < 2 {
651 return Err(DriverError::Protocol("DataRow too short".into()));
652 }
653 let num_cols = i16::from_be_bytes([data[0], data[1]]);
654 if num_cols < 0 {
655 return Err(DriverError::Protocol(
656 "DataRow: negative column count".into(),
657 ));
658 }
659 let num_cols = num_cols as usize;
660 let mut offsets = smallvec::SmallVec::<[(usize, i32); 16]>::with_capacity(num_cols);
661 let mut pos = 2usize;
662 for _ in 0..num_cols {
663 if pos + 4 > data.len() {
664 return Err(DriverError::Protocol("DataRow truncated".into()));
665 }
666 let col_len =
667 i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
668 pos += 4;
669 offsets.push((pos, col_len));
670 if col_len > 0 {
671 pos += col_len as usize;
672 }
673 }
674 Ok(Self { data, offsets })
675 }
676
677 #[inline]
679 pub fn get_raw(&self, idx: usize) -> Option<&'a [u8]> {
680 let (offset, len) = self.offsets[idx];
681 if len < 0 {
682 None
683 } else {
684 Some(&self.data[offset..offset + len as usize])
685 }
686 }
687
688 #[inline]
690 pub fn is_null(&self, idx: usize) -> bool {
691 self.offsets[idx].1 < 0
692 }
693
694 #[inline]
696 pub fn column_count(&self) -> usize {
697 self.offsets.len()
698 }
699
700 #[inline]
702 pub fn get_bool(&self, idx: usize) -> Option<bool> {
703 self.get_raw(idx)
704 .and_then(|data| crate::codec::decode_bool(data).ok())
705 }
706
707 #[inline]
709 pub fn get_i16(&self, idx: usize) -> Option<i16> {
710 self.get_raw(idx)
711 .and_then(|data| crate::codec::decode_i16(data).ok())
712 }
713
714 #[inline]
716 pub fn get_i32(&self, idx: usize) -> Option<i32> {
717 self.get_raw(idx)
718 .and_then(|data| crate::codec::decode_i32(data).ok())
719 }
720
721 #[inline]
723 pub fn get_i64(&self, idx: usize) -> Option<i64> {
724 self.get_raw(idx)
725 .and_then(|data| crate::codec::decode_i64(data).ok())
726 }
727
728 #[inline]
730 pub fn get_f32(&self, idx: usize) -> Option<f32> {
731 self.get_raw(idx)
732 .and_then(|data| crate::codec::decode_f32(data).ok())
733 }
734
735 #[inline]
737 pub fn get_f64(&self, idx: usize) -> Option<f64> {
738 self.get_raw(idx)
739 .and_then(|data| crate::codec::decode_f64(data).ok())
740 }
741
742 #[inline]
744 pub fn get_str(&self, idx: usize) -> Option<&'a str> {
745 self.get_raw(idx)
746 .and_then(|data| crate::codec::decode_str(data).ok())
747 }
748
749 #[inline]
751 pub fn get_bytes(&self, idx: usize) -> Option<&'a [u8]> {
752 self.get_raw(idx)
753 }
754}
755
756pub fn hash_sql(sql: &str) -> u64 {
773 use std::hash::{Hash, Hasher};
774 let mut hasher = RapidHasher::default();
775 sql.hash(&mut hasher);
776 hasher.finish()
777}
778
779#[cfg(test)]
784#[allow(clippy::approx_constant)]
785mod tests {
786 use super::*;
787
788 #[test]
793 fn config_parse_full_url() {
794 let cfg = Config::from_url("postgres://user:pass@localhost:5432/mydb").unwrap();
795 assert_eq!(cfg.user, "user");
796 assert_eq!(cfg.password, "pass");
797 assert_eq!(cfg.host, "localhost");
798 assert_eq!(cfg.port, 5432);
799 assert_eq!(cfg.database, "mydb");
800 }
801
802 #[test]
803 fn config_parse_default_port() {
804 let cfg = Config::from_url("postgres://user:pass@localhost/mydb").unwrap();
805 assert_eq!(cfg.port, 5432);
806 }
807
808 #[test]
809 fn config_parse_no_password() {
810 let cfg = Config::from_url("postgres://user@localhost/mydb").unwrap();
811 assert_eq!(cfg.user, "user");
812 assert_eq!(cfg.password, "");
813 }
814
815 #[test]
816 fn config_parse_empty_database() {
817 let cfg = Config::from_url("postgres://user:pass@localhost").unwrap();
818 assert_eq!(cfg.database, "user");
820 }
821
822 #[test]
823 fn config_parse_sslmode() {
824 let cfg = Config::from_url("postgres://user:pass@localhost/db?sslmode=require").unwrap();
825 assert_eq!(cfg.ssl, SslMode::Require);
826 }
827
828 #[test]
829 fn config_parse_percent_encoding() {
830 let cfg = Config::from_url("postgres://user%40domain:p%40ss@localhost/db").unwrap();
831 assert_eq!(cfg.user, "user@domain");
832 assert_eq!(cfg.password, "p@ss");
833 }
834
835 #[test]
836 fn config_rejects_bad_scheme() {
837 let result = Config::from_url("mysql://user:pass@localhost/db");
838 assert!(result.is_err());
839 }
840
841 #[test]
843 fn config_rejects_unknown_sslmode() {
844 let result = Config::from_url("postgres://user:pass@localhost/db?sslmode=requre");
845 assert!(result.is_err(), "typo 'requre' should be rejected");
846 let result = Config::from_url("postgres://user:pass@localhost/db?sslmode=REQUIRE");
847 assert!(result.is_err(), "uppercase should be rejected");
848 let result = Config::from_url("postgres://user:pass@localhost/db?sslmode=bogus");
849 assert!(result.is_err(), "bogus value should be rejected");
850 }
851
852 #[test]
854 fn config_accepts_valid_sslmodes() {
855 let cfg = Config::from_url("postgres://user:pass@localhost/db?sslmode=disable").unwrap();
856 assert_eq!(cfg.ssl, SslMode::Disable);
857 let cfg = Config::from_url("postgres://user:pass@localhost/db?sslmode=prefer").unwrap();
858 assert_eq!(cfg.ssl, SslMode::Prefer);
859 let cfg = Config::from_url("postgres://user:pass@localhost/db?sslmode=require").unwrap();
860 assert_eq!(cfg.ssl, SslMode::Require);
861 }
862
863 #[test]
865 fn config_parse_postgresql_scheme() {
866 let cfg = Config::from_url("postgresql://user:pass@localhost:5432/mydb").unwrap();
867 assert_eq!(cfg.user, "user");
868 assert_eq!(cfg.password, "pass");
869 assert_eq!(cfg.host, "localhost");
870 assert_eq!(cfg.port, 5432);
871 assert_eq!(cfg.database, "mydb");
872 }
873
874 #[test]
876 fn config_parse_no_password_standalone() {
877 let cfg = Config::from_url("postgres://admin@db.example.com/myapp").unwrap();
878 assert_eq!(cfg.user, "admin");
879 assert_eq!(cfg.password, "");
880 assert_eq!(cfg.host, "db.example.com");
881 assert_eq!(cfg.database, "myapp");
882 }
883
884 #[test]
886 fn config_empty_database_falls_back_to_user() {
887 let cfg = Config::from_url("postgres://testuser:pass@localhost").unwrap();
888 assert_eq!(cfg.database, "testuser");
889 }
890
891 #[test]
893 fn config_unknown_sslmode_error() {
894 let result = Config::from_url("postgres://u:p@h/d?sslmode=verify-full");
895 assert!(result.is_err());
896 let err = result.unwrap_err().to_string();
897 assert!(
898 err.contains("unknown sslmode"),
899 "should describe unknown sslmode: {err}"
900 );
901 }
902
903 #[test]
905 fn config_multiple_query_params() {
906 let cfg = Config::from_url(
907 "postgres://user:pass@localhost/db?sslmode=disable&statement_timeout=60",
908 )
909 .unwrap();
910 assert_eq!(cfg.ssl, SslMode::Disable);
911 assert_eq!(cfg.statement_timeout_secs, 60);
912 }
913
914 #[test]
916 fn config_validate_empty_host() {
917 let cfg = Config {
918 host: String::new(),
919 port: 5432,
920 user: "user".into(),
921 password: "pass".into(),
922 database: "db".into(),
923 ssl: SslMode::Disable,
924 statement_timeout_secs: 30,
925 statement_cache_mode: StatementCacheMode::Named,
926 ssl_root_cert: None,
927 ssl_cert: None,
928 ssl_key: None,
929 };
930 assert!(cfg.validate().is_err());
931 }
932
933 #[test]
935 fn config_validate_empty_user() {
936 let cfg = Config {
937 host: "localhost".into(),
938 port: 5432,
939 user: String::new(),
940 password: "pass".into(),
941 database: "db".into(),
942 ssl: SslMode::Disable,
943 statement_timeout_secs: 30,
944 statement_cache_mode: StatementCacheMode::Named,
945 ssl_root_cert: None,
946 ssl_cert: None,
947 ssl_key: None,
948 };
949 assert!(cfg.validate().is_err());
950 }
951
952 #[test]
954 fn config_validate_empty_database() {
955 let cfg = Config {
956 host: "localhost".into(),
957 port: 5432,
958 user: "user".into(),
959 password: "pass".into(),
960 database: String::new(),
961 ssl: SslMode::Disable,
962 statement_timeout_secs: 30,
963 statement_cache_mode: StatementCacheMode::Named,
964 ssl_root_cert: None,
965 ssl_cert: None,
966 ssl_key: None,
967 };
968 assert!(cfg.validate().is_err());
969 }
970
971 #[test]
973 fn config_missing_at_sign() {
974 let result = Config::from_url("postgres://userpasslocalhost/db");
975 assert!(result.is_err());
976 }
977
978 #[test]
980 fn config_custom_port() {
981 let cfg = Config::from_url("postgres://user:pass@localhost:5433/db").unwrap();
982 assert_eq!(cfg.port, 5433);
983 }
984
985 #[test]
987 fn config_invalid_port() {
988 let result = Config::from_url("postgres://user:pass@localhost:notaport/db");
989 assert!(result.is_err());
990 }
991
992 #[cfg(not(feature = "tls"))]
994 #[test]
995 fn config_sslmode_require_without_tls_feature() {
996 let cfg = Config::from_url("postgres://user:pass@localhost/db?sslmode=require").unwrap();
999 assert_eq!(cfg.ssl, SslMode::Require);
1000 }
1001
1002 #[test]
1003 fn config_statement_timeout_default() {
1004 let cfg = Config::from_url("postgres://user:pass@localhost/db").unwrap();
1005 assert_eq!(cfg.statement_timeout_secs, 30);
1006 }
1007
1008 #[test]
1009 fn config_statement_timeout_custom() {
1010 let cfg =
1011 Config::from_url("postgres://user:pass@localhost/db?statement_timeout=120").unwrap();
1012 assert_eq!(cfg.statement_timeout_secs, 120);
1013 }
1014
1015 #[test]
1016 fn config_statement_timeout_zero() {
1017 let cfg =
1018 Config::from_url("postgres://user:pass@localhost/db?statement_timeout=0").unwrap();
1019 assert_eq!(cfg.statement_timeout_secs, 0);
1020 }
1021
1022 #[test]
1023 fn config_statement_timeout_invalid_falls_back() {
1024 let cfg =
1025 Config::from_url("postgres://user:pass@localhost/db?statement_timeout=notanumber")
1026 .unwrap();
1027 assert_eq!(cfg.statement_timeout_secs, 30); }
1029
1030 #[test]
1035 fn parse_statement_cache_default() {
1036 let cfg = Config::from_url("postgres://user:pass@localhost/db").unwrap();
1037 assert_eq!(cfg.statement_cache_mode, StatementCacheMode::Named);
1038 }
1039
1040 #[test]
1041 fn parse_statement_cache_named() {
1042 let cfg =
1043 Config::from_url("postgres://user:pass@localhost/db?statement_cache=named").unwrap();
1044 assert_eq!(cfg.statement_cache_mode, StatementCacheMode::Named);
1045 }
1046
1047 #[test]
1048 fn parse_statement_cache_disabled() {
1049 let cfg =
1050 Config::from_url("postgres://user:pass@localhost/db?statement_cache=disabled").unwrap();
1051 assert_eq!(cfg.statement_cache_mode, StatementCacheMode::Disabled);
1052 }
1053
1054 #[test]
1055 fn parse_statement_cache_invalid() {
1056 let result = Config::from_url("postgres://user:pass@localhost/db?statement_cache=off");
1057 assert!(result.is_err(), "invalid value 'off' should be rejected");
1058 let result = Config::from_url("postgres://user:pass@localhost/db?statement_cache=DISABLED");
1059 assert!(result.is_err(), "uppercase should be rejected");
1060 let result = Config::from_url("postgres://user:pass@localhost/db?statement_cache=bogus");
1061 assert!(result.is_err(), "bogus value should be rejected");
1062 }
1063
1064 #[test]
1065 fn parse_statement_cache_with_other_params() {
1066 let cfg = Config::from_url(
1067 "postgres://user:pass@localhost/db?sslmode=disable&statement_cache=disabled&statement_timeout=60",
1068 )
1069 .unwrap();
1070 assert_eq!(cfg.statement_cache_mode, StatementCacheMode::Disabled);
1071 assert_eq!(cfg.ssl, SslMode::Disable);
1072 assert_eq!(cfg.statement_timeout_secs, 60);
1073 }
1074
1075 #[test]
1076 fn statement_cache_mode_default_is_named() {
1077 assert_eq!(StatementCacheMode::default(), StatementCacheMode::Named);
1078 }
1079
1080 #[test]
1085 fn parse_ssl_root_cert() {
1086 let cfg = Config::from_url("postgres://user:pass@localhost/db?sslrootcert=/path/to/ca.pem")
1087 .unwrap();
1088 assert_eq!(cfg.ssl_root_cert.as_deref(), Some("/path/to/ca.pem"));
1089 assert_eq!(cfg.ssl_cert, None);
1090 assert_eq!(cfg.ssl_key, None);
1091 }
1092
1093 #[test]
1094 fn parse_ssl_cert_and_key() {
1095 let cfg = Config::from_url(
1096 "postgres://user:pass@localhost/db?sslcert=/path/to/client.pem&sslkey=/path/to/client.key",
1097 )
1098 .unwrap();
1099 assert_eq!(cfg.ssl_root_cert, None);
1100 assert_eq!(cfg.ssl_cert.as_deref(), Some("/path/to/client.pem"));
1101 assert_eq!(cfg.ssl_key.as_deref(), Some("/path/to/client.key"));
1102 }
1103
1104 #[test]
1105 fn parse_ssl_all_tls_params() {
1106 let cfg = Config::from_url(
1107 "postgres://user:pass@localhost/db?sslmode=require&sslrootcert=/ca.pem&sslcert=/client.pem&sslkey=/client.key",
1108 )
1109 .unwrap();
1110 assert_eq!(cfg.ssl, SslMode::Require);
1111 assert_eq!(cfg.ssl_root_cert.as_deref(), Some("/ca.pem"));
1112 assert_eq!(cfg.ssl_cert.as_deref(), Some("/client.pem"));
1113 assert_eq!(cfg.ssl_key.as_deref(), Some("/client.key"));
1114 }
1115
1116 #[test]
1117 fn parse_ssl_paths_percent_encoded() {
1118 let cfg = Config::from_url("postgres://user:pass@localhost/db?sslrootcert=%2Ftmp%2Fca.pem")
1120 .unwrap();
1121 assert_eq!(cfg.ssl_root_cert.as_deref(), Some("/tmp/ca.pem"));
1122 }
1123
1124 #[test]
1125 fn parse_ssl_params_default_none() {
1126 let cfg = Config::from_url("postgres://user:pass@localhost/db").unwrap();
1127 assert_eq!(cfg.ssl_root_cert, None);
1128 assert_eq!(cfg.ssl_cert, None);
1129 assert_eq!(cfg.ssl_key, None);
1130 }
1131
1132 #[test]
1133 fn config_uds_path_format() {
1134 let cfg = Config::from_url("postgres://user@localhost/db?host=/tmp").unwrap();
1135 assert_eq!(cfg.uds_path(), "/tmp/.s.PGSQL.5432");
1136 }
1137
1138 #[test]
1139 fn config_uds_path_custom_port() {
1140 let cfg = Config::from_url("postgres://user@localhost:5433/db?host=/tmp").unwrap();
1141 assert_eq!(cfg.uds_path(), "/tmp/.s.PGSQL.5433");
1142 }
1143
1144 #[test]
1149 fn config_host_is_uds_absolute_path() {
1150 let cfg = Config {
1151 host: "/tmp".into(),
1152 port: 5432,
1153 user: "user".into(),
1154 password: "".into(),
1155 database: "db".into(),
1156 ssl: SslMode::Disable,
1157 statement_timeout_secs: 30,
1158 statement_cache_mode: StatementCacheMode::Named,
1159 ssl_root_cert: None,
1160 ssl_cert: None,
1161 ssl_key: None,
1162 };
1163 assert!(cfg.host_is_uds());
1164 assert_eq!(cfg.uds_path(), "/tmp/.s.PGSQL.5432");
1165 }
1166
1167 #[test]
1168 fn config_host_is_uds_var_run() {
1169 let cfg = Config {
1170 host: "/var/run/postgresql".into(),
1171 port: 5433,
1172 user: "user".into(),
1173 password: "".into(),
1174 database: "db".into(),
1175 ssl: SslMode::Disable,
1176 statement_timeout_secs: 30,
1177 statement_cache_mode: StatementCacheMode::Named,
1178 ssl_root_cert: None,
1179 ssl_cert: None,
1180 ssl_key: None,
1181 };
1182 assert!(cfg.host_is_uds());
1183 assert_eq!(cfg.uds_path(), "/var/run/postgresql/.s.PGSQL.5433");
1184 }
1185
1186 #[test]
1187 fn config_host_is_not_uds_for_hostname() {
1188 let cfg = Config {
1189 host: "localhost".into(),
1190 port: 5432,
1191 user: "user".into(),
1192 password: "".into(),
1193 database: "db".into(),
1194 ssl: SslMode::Disable,
1195 statement_timeout_secs: 30,
1196 statement_cache_mode: StatementCacheMode::Named,
1197 ssl_root_cert: None,
1198 ssl_cert: None,
1199 ssl_key: None,
1200 };
1201 assert!(!cfg.host_is_uds());
1202 }
1203
1204 #[test]
1205 fn config_host_is_not_uds_for_ip() {
1206 let cfg = Config {
1207 host: "127.0.0.1".into(),
1208 port: 5432,
1209 user: "user".into(),
1210 password: "".into(),
1211 database: "db".into(),
1212 ssl: SslMode::Disable,
1213 statement_timeout_secs: 30,
1214 statement_cache_mode: StatementCacheMode::Named,
1215 ssl_root_cert: None,
1216 ssl_cert: None,
1217 ssl_key: None,
1218 };
1219 assert!(!cfg.host_is_uds());
1220 }
1221
1222 #[test]
1223 fn config_parse_uds_host_query_param() {
1224 let cfg = Config::from_url("postgres://user@localhost/mydb?host=/tmp").unwrap();
1225 assert_eq!(cfg.host, "/tmp");
1226 assert!(cfg.host_is_uds());
1227 assert_eq!(cfg.uds_path(), "/tmp/.s.PGSQL.5432");
1228 assert_eq!(cfg.database, "mydb");
1229 assert_eq!(cfg.user, "user");
1230 }
1231
1232 #[test]
1233 fn config_parse_uds_host_query_param_custom_port() {
1234 let cfg = Config::from_url("postgres://user@localhost:5433/mydb?host=/var/run/postgresql")
1235 .unwrap();
1236 assert_eq!(cfg.host, "/var/run/postgresql");
1237 assert_eq!(cfg.port, 5433);
1238 assert_eq!(cfg.uds_path(), "/var/run/postgresql/.s.PGSQL.5433");
1239 }
1240
1241 #[test]
1242 fn config_parse_uds_host_with_other_params() {
1243 let cfg = Config::from_url(
1244 "postgres://user@localhost/db?host=/tmp&sslmode=disable&statement_timeout=60",
1245 )
1246 .unwrap();
1247 assert_eq!(cfg.host, "/tmp");
1248 assert!(cfg.host_is_uds());
1249 assert_eq!(cfg.ssl, SslMode::Disable);
1250 assert_eq!(cfg.statement_timeout_secs, 60);
1251 }
1252
1253 #[test]
1254 fn config_parse_uds_host_percent_encoded() {
1255 let cfg = Config::from_url("postgres://user@localhost/db?host=%2Ftmp").unwrap();
1257 assert_eq!(cfg.host, "/tmp");
1258 assert!(cfg.host_is_uds());
1259 }
1260
1261 #[test]
1262 fn config_parse_tcp_host_not_overridden_without_param() {
1263 let cfg = Config::from_url("postgres://user@myserver/db").unwrap();
1265 assert_eq!(cfg.host, "myserver");
1266 assert!(!cfg.host_is_uds());
1267 }
1268
1269 #[test]
1270 fn config_parse_uds_host_overrides_url_hostname() {
1271 let cfg = Config::from_url("postgres://user@db.example.com/mydb?host=/var/run/postgresql")
1273 .unwrap();
1274 assert_eq!(cfg.host, "/var/run/postgresql");
1275 assert!(cfg.host_is_uds());
1276 }
1277
1278 #[test]
1279 fn config_parse_uds_empty_url_host() {
1280 let cfg = Config::from_url("postgres://user@/mydb?host=/tmp").unwrap();
1282 assert_eq!(cfg.host, "/tmp");
1283 assert!(cfg.host_is_uds());
1284 assert_eq!(cfg.database, "mydb");
1285 }
1286
1287 #[test]
1292 fn url_decode_works() {
1293 assert_eq!(url_decode("hello%20world").unwrap(), "hello world");
1294 assert_eq!(url_decode("no%20escape").unwrap(), "no escape");
1295 assert_eq!(url_decode("plain").unwrap(), "plain");
1296 assert_eq!(url_decode("a%40b").unwrap(), "a@b");
1297 }
1298
1299 #[test]
1300 fn url_decode_malformed_percent_trailing() {
1301 let result = url_decode("abc%2");
1303 assert!(result.is_err(), "truncated %2 should error");
1304 }
1305
1306 #[test]
1307 fn url_decode_malformed_percent_no_digits() {
1308 let result = url_decode("abc%");
1310 assert!(result.is_err(), "bare % at end should error");
1311 }
1312
1313 #[test]
1314 fn url_decode_invalid_hex_digit() {
1315 let result = url_decode("abc%GG");
1317 assert!(result.is_err(), "%GG should error");
1318 }
1319
1320 #[test]
1321 fn url_decode_invalid_hex_second_digit() {
1322 let result = url_decode("abc%2Z");
1324 assert!(result.is_err(), "%2Z should error");
1325 }
1326
1327 #[test]
1329 fn url_decode_invalid_utf8_percent() {
1330 let result = url_decode("%80%81");
1332 assert!(result.is_err(), "invalid UTF-8 bytes should error");
1333 }
1334
1335 #[test]
1337 fn url_decode_percent_everywhere() {
1338 assert_eq!(url_decode("%41%42%43").unwrap(), "ABC");
1339 assert_eq!(url_decode("%61").unwrap(), "a");
1340 assert_eq!(url_decode("x%2Fy%2Fz").unwrap(), "x/y/z");
1341 }
1342
1343 #[test]
1345 fn url_decode_bare_percent_middle() {
1346 assert!(url_decode("a%b").is_err(), "bare % in middle should error");
1347 }
1348
1349 #[test]
1351 fn url_decode_multibyte_utf8() {
1352 let result = url_decode("caf%C3%A9").unwrap();
1353 assert_eq!(result, "caf\u{00e9}"); }
1355
1356 #[test]
1358 fn url_decode_invalid_percent_zz() {
1359 let result = url_decode("abc%ZZ");
1360 assert!(result.is_err(), "%ZZ should error");
1361 }
1362
1363 #[test]
1365 fn url_decode_truncated_percent_trailing() {
1366 let result = url_decode("abc%");
1367 assert!(result.is_err(), "trailing % should error");
1368 }
1369
1370 #[test]
1372 fn url_decode_invalid_utf8() {
1373 let result = url_decode("%80");
1375 assert!(result.is_err(), "invalid UTF-8 should error");
1376 }
1377
1378 #[test]
1379 fn url_decode_empty_string() {
1380 assert_eq!(url_decode("").unwrap(), "");
1381 }
1382
1383 #[test]
1384 fn url_decode_no_encoding() {
1385 assert_eq!(url_decode("hello").unwrap(), "hello");
1386 }
1387
1388 #[test]
1389 fn url_decode_all_ascii_hex() {
1390 assert_eq!(url_decode("%2F").unwrap(), "/");
1392 assert_eq!(url_decode("%2f").unwrap(), "/");
1393 }
1394
1395 #[test]
1399 fn config_unicode_password() {
1400 let cfg =
1402 Config::from_url("postgres://user:%D0%BF%D0%B0%D1%80%D0%BE%D0%BB%D1%8C@localhost/db")
1403 .unwrap();
1404 assert_eq!(cfg.user, "user");
1405 assert_eq!(
1406 cfg.password,
1407 "\u{043F}\u{0430}\u{0440}\u{043E}\u{043B}\u{044C}"
1408 ); assert_eq!(cfg.host, "localhost");
1410 assert_eq!(cfg.database, "db");
1411 }
1412
1413 #[test]
1415 fn config_port_zero() {
1416 let cfg = Config::from_url("postgres://user:pass@localhost:0/db").unwrap();
1417 assert_eq!(cfg.port, 0);
1418 }
1419
1420 #[test]
1422 fn config_port_max() {
1423 let cfg = Config::from_url("postgres://user:pass@localhost:65535/db").unwrap();
1424 assert_eq!(cfg.port, 65535);
1425 }
1426
1427 #[test]
1429 fn config_port_overflow() {
1430 let result = Config::from_url("postgres://user:pass@localhost:65536/db");
1431 assert!(result.is_err(), "port 65536 exceeds u16 max");
1432 }
1433
1434 #[test]
1436 fn config_unknown_param_ignored() {
1437 let cfg = Config::from_url(
1438 "postgres://user:pass@localhost/db?application_name=myapp&connect_timeout=10",
1439 )
1440 .unwrap();
1441 assert_eq!(cfg.user, "user");
1443 assert_eq!(cfg.host, "localhost");
1444 assert_eq!(cfg.database, "db");
1445 assert_eq!(cfg.statement_timeout_secs, 30);
1447 assert_eq!(cfg.ssl, SslMode::Prefer);
1448 }
1449
1450 #[test]
1452 fn url_decode_double_percent_encoding() {
1453 assert_eq!(url_decode("%2525").unwrap(), "%25");
1455 }
1456
1457 #[test]
1459 fn config_explicit_empty_password() {
1460 let cfg = Config::from_url("postgres://user:@localhost/db").unwrap();
1461 assert_eq!(cfg.user, "user");
1462 assert_eq!(cfg.password, "");
1463 }
1464
1465 #[test]
1467 fn config_special_chars_in_user() {
1468 let cfg = Config::from_url("postgres://my%2Fuser:pass@localhost/my%2Fdb").unwrap();
1469 assert_eq!(cfg.user, "my/user");
1470 assert_eq!(cfg.database, "my/db");
1471 }
1472
1473 #[test]
1475 fn url_decode_plus_is_literal() {
1476 assert_eq!(url_decode("a+b").unwrap(), "a+b");
1477 }
1478
1479 #[test]
1481 fn config_minimal_valid_url() {
1482 let cfg = Config::from_url("postgres://user@localhost/db").unwrap();
1483 assert_eq!(cfg.user, "user");
1484 assert_eq!(cfg.password, "");
1485 assert_eq!(cfg.host, "localhost");
1486 assert_eq!(cfg.port, 5432);
1487 assert_eq!(cfg.database, "db");
1488 }
1489
1490 #[test]
1492 fn config_empty_param_segments() {
1493 let cfg =
1494 Config::from_url("postgres://user:pass@localhost/db?&&statement_timeout=60&&").unwrap();
1495 assert_eq!(cfg.statement_timeout_secs, 60);
1496 }
1497
1498 #[test]
1503 fn hash_sql_deterministic() {
1504 let h1 = hash_sql("SELECT 1");
1505 let h2 = hash_sql("SELECT 1");
1506 assert_eq!(h1, h2);
1507 }
1508
1509 #[test]
1510 fn hash_sql_different_queries() {
1511 let h1 = hash_sql("SELECT 1");
1512 let h2 = hash_sql("SELECT 2");
1513 assert_ne!(h1, h2);
1514 }
1515
1516 #[test]
1517 fn hash_sql_empty() {
1518 let _h = hash_sql(""); }
1520
1521 #[test]
1522 fn hash_sql_whitespace_only() {
1523 let h = hash_sql(" ");
1524 assert_ne!(h, hash_sql(""));
1525 }
1526
1527 #[test]
1528 fn hash_sql_very_long() {
1529 let long_sql = "SELECT ".to_string() + &"x".repeat(10_000);
1530 let h = hash_sql(&long_sql);
1531 assert_eq!(h, hash_sql(&long_sql));
1532 }
1533
1534 #[test]
1535 fn hash_sql_unicode() {
1536 let h = hash_sql("SELECT '\u{1F600}'");
1537 assert_ne!(h, hash_sql("SELECT 'x'"));
1538 }
1539
1540 #[test]
1545 fn notification_struct_fields() {
1546 let n = Notification {
1547 pid: 42,
1548 channel: "test_chan".to_owned(),
1549 payload: "hello".to_owned(),
1550 };
1551 assert_eq!(n.pid, 42);
1552 assert_eq!(n.channel, "test_chan");
1553 assert_eq!(n.payload, "hello");
1554 }
1555
1556 #[test]
1557 fn notification_clone() {
1558 let n = Notification {
1559 pid: 1,
1560 channel: "c".to_owned(),
1561 payload: "p".to_owned(),
1562 };
1563 let n2 = n.clone();
1564 assert_eq!(n2.pid, 1);
1565 assert_eq!(n2.channel, "c");
1566 }
1567
1568 #[test]
1569 fn notification_debug() {
1570 let n = Notification {
1571 pid: 1,
1572 channel: "c".to_owned(),
1573 payload: "p".to_owned(),
1574 };
1575 let dbg = format!("{n:?}");
1576 assert!(dbg.contains("Notification"));
1577 }
1578
1579 #[test]
1584 fn query_result_empty() {
1585 let result = QueryResult {
1586 all_col_offsets: vec![],
1587 num_cols: 0,
1588 columns: Arc::from(Vec::new()),
1589 affected_rows: 0,
1590 data_buf: None,
1591 };
1592 assert!(result.is_empty());
1593 assert_eq!(result.len(), 0);
1594 }
1595
1596 #[test]
1597 fn query_result_from_parts() {
1598 let result = QueryResult::from_parts(vec![(0, 4), (0, -1)], 2, Arc::from(Vec::new()), 5);
1599 assert_eq!(result.len(), 1);
1600 assert_eq!(result.num_cols, 2);
1601 assert_eq!(result.affected_rows, 5);
1602 }
1603
1604 #[test]
1605 fn query_result_affected_rows() {
1606 let result = QueryResult {
1607 all_col_offsets: vec![],
1608 num_cols: 0,
1609 columns: Arc::from(Vec::new()),
1610 affected_rows: 42,
1611 data_buf: None,
1612 };
1613 assert_eq!(result.affected_rows, 42);
1614 assert!(result.is_empty());
1615 }
1616
1617 fn make_data_row(columns: &[Option<&[u8]>]) -> Vec<u8> {
1624 let mut buf = Vec::new();
1625 buf.extend_from_slice(&(columns.len() as i16).to_be_bytes());
1626 for col in columns {
1627 match col {
1628 Some(data) => {
1629 buf.extend_from_slice(&(data.len() as i32).to_be_bytes());
1630 buf.extend_from_slice(data);
1631 }
1632 None => {
1633 buf.extend_from_slice(&(-1i32).to_be_bytes());
1634 }
1635 }
1636 }
1637 buf
1638 }
1639
1640 #[test]
1641 fn pg_data_row_get_i32() {
1642 let data = make_data_row(&[Some(&42i32.to_be_bytes())]);
1643 let row = PgDataRow::new(&data).unwrap();
1644 assert_eq!(row.get_i32(0), Some(42));
1645 assert_eq!(row.column_count(), 1);
1646 }
1647
1648 #[test]
1649 fn pg_data_row_get_i64() {
1650 let data = make_data_row(&[Some(&12345i64.to_be_bytes())]);
1651 let row = PgDataRow::new(&data).unwrap();
1652 assert_eq!(row.get_i64(0), Some(12345));
1653 }
1654
1655 #[test]
1656 fn pg_data_row_get_str() {
1657 let data = make_data_row(&[Some(b"hello")]);
1658 let row = PgDataRow::new(&data).unwrap();
1659 assert_eq!(row.get_str(0), Some("hello"));
1660 }
1661
1662 #[test]
1663 fn pg_data_row_get_bytes() {
1664 let data = make_data_row(&[Some(&[0xDE, 0xAD, 0xBE, 0xEF])]);
1665 let row = PgDataRow::new(&data).unwrap();
1666 assert_eq!(row.get_bytes(0), Some(&[0xDE, 0xAD, 0xBE, 0xEF][..]));
1667 }
1668
1669 #[test]
1670 fn pg_data_row_get_bool() {
1671 let data = make_data_row(&[Some(&[1u8])]);
1672 let row = PgDataRow::new(&data).unwrap();
1673 assert_eq!(row.get_bool(0), Some(true));
1674
1675 let data = make_data_row(&[Some(&[0u8])]);
1676 let row = PgDataRow::new(&data).unwrap();
1677 assert_eq!(row.get_bool(0), Some(false));
1678 }
1679
1680 #[test]
1681 fn pg_data_row_get_f64() {
1682 let data = make_data_row(&[Some(&3.14f64.to_be_bytes())]);
1683 let row = PgDataRow::new(&data).unwrap();
1684 assert!((row.get_f64(0).unwrap() - 3.14).abs() < 1e-10);
1685 }
1686
1687 #[test]
1688 fn pg_data_row_null_column() {
1689 let data = make_data_row(&[None]);
1690 let row = PgDataRow::new(&data).unwrap();
1691 assert!(row.is_null(0));
1692 assert_eq!(row.get_i32(0), None);
1693 assert_eq!(row.get_str(0), None);
1694 }
1695
1696 #[test]
1697 fn pg_data_row_multiple_columns() {
1698 let data = make_data_row(&[
1699 Some(&42i32.to_be_bytes()),
1700 Some(b"alice"),
1701 Some(b"alice@example.com"),
1702 Some(&[1u8]),
1703 Some(&3.14f64.to_be_bytes()),
1704 ]);
1705 let row = PgDataRow::new(&data).unwrap();
1706 assert_eq!(row.column_count(), 5);
1707 assert_eq!(row.get_i32(0), Some(42));
1708 assert_eq!(row.get_str(1), Some("alice"));
1709 assert_eq!(row.get_str(2), Some("alice@example.com"));
1710 assert_eq!(row.get_bool(3), Some(true));
1711 assert!((row.get_f64(4).unwrap() - 3.14).abs() < 1e-10);
1712 }
1713
1714 #[test]
1715 fn pg_data_row_mixed_null() {
1716 let data = make_data_row(&[Some(&42i32.to_be_bytes()), None, Some(b"text")]);
1717 let row = PgDataRow::new(&data).unwrap();
1718 assert_eq!(row.get_i32(0), Some(42));
1719 assert!(row.is_null(1));
1720 assert_eq!(row.get_str(1), None);
1721 assert_eq!(row.get_str(2), Some("text"));
1722 }
1723
1724 #[test]
1725 fn pg_data_row_empty() {
1726 let data = make_data_row(&[]);
1727 let row = PgDataRow::new(&data).unwrap();
1728 assert_eq!(row.column_count(), 0);
1729 }
1730
1731 #[test]
1732 fn pg_data_row_too_short() {
1733 let data = vec![0u8]; assert!(PgDataRow::new(&data).is_err());
1735 }
1736
1737 #[test]
1738 fn pg_data_row_truncated() {
1739 let mut data = Vec::new();
1741 data.extend_from_slice(&2i16.to_be_bytes());
1742 data.extend_from_slice(&4i32.to_be_bytes());
1743 data.extend_from_slice(&42i32.to_be_bytes());
1744 assert!(PgDataRow::new(&data).is_err());
1746 }
1747
1748 #[test]
1749 fn pg_data_row_get_i16() {
1750 let data = make_data_row(&[Some(&7i16.to_be_bytes())]);
1751 let row = PgDataRow::new(&data).unwrap();
1752 assert_eq!(row.get_i16(0), Some(7));
1753 }
1754
1755 #[test]
1756 fn pg_data_row_get_f32() {
1757 let data = make_data_row(&[Some(&2.5f32.to_be_bytes())]);
1758 let row = PgDataRow::new(&data).unwrap();
1759 assert!((row.get_f32(0).unwrap() - 2.5).abs() < 1e-6);
1760 }
1761
1762 #[test]
1763 fn pg_data_row_get_raw_null() {
1764 let data = make_data_row(&[None]);
1765 let row = PgDataRow::new(&data).unwrap();
1766 assert_eq!(row.get_raw(0), None);
1767 }
1768
1769 #[test]
1770 fn pg_data_row_get_raw_data() {
1771 let data = make_data_row(&[Some(&[1, 2, 3])]);
1772 let row = PgDataRow::new(&data).unwrap();
1773 assert_eq!(row.get_raw(0), Some(&[1u8, 2, 3][..]));
1774 }
1775
1776 #[test]
1777 fn pg_data_row_stack_alloc_16_columns() {
1778 let cols: Vec<Option<&[u8]>> = (0..16).map(|_| Some(&[0u8][..])).collect();
1780 let data = make_data_row(&cols);
1781 let row = PgDataRow::new(&data).unwrap();
1782 assert_eq!(row.column_count(), 16);
1783 for i in 0..16 {
1785 assert_eq!(row.get_raw(i), Some(&[0u8][..]));
1786 }
1787 }
1788
1789 #[test]
1794 fn inline_sequential_decode_five_columns() {
1795 let data = make_data_row(&[
1796 Some(&42i32.to_be_bytes()),
1797 Some(b"alice"),
1798 Some(b"alice@example.com"),
1799 Some(&[1u8]),
1800 Some(&3.14f64.to_be_bytes()),
1801 ]);
1802
1803 let mut pos: usize = 2; let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1808 pos += 4;
1809 assert_eq!(len, 4);
1810 let id = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1811 pos += len as usize;
1812 assert_eq!(id, 42);
1813
1814 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1816 pos += 4;
1817 assert_eq!(len, 5);
1818 let name = std::str::from_utf8(&data[pos..pos + len as usize]).unwrap();
1819 pos += len as usize;
1820 assert_eq!(name, "alice");
1821
1822 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1824 pos += 4;
1825 let email = std::str::from_utf8(&data[pos..pos + len as usize]).unwrap();
1826 pos += len as usize;
1827 assert_eq!(email, "alice@example.com");
1828
1829 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1831 pos += 4;
1832 assert_eq!(len, 1);
1833 let active = data[pos] != 0;
1834 pos += len as usize;
1835 assert!(active);
1836
1837 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1839 pos += 4;
1840 assert_eq!(len, 8);
1841 let score = f64::from_be_bytes([
1842 data[pos],
1843 data[pos + 1],
1844 data[pos + 2],
1845 data[pos + 3],
1846 data[pos + 4],
1847 data[pos + 5],
1848 data[pos + 6],
1849 data[pos + 7],
1850 ]);
1851 pos += len as usize;
1852 assert!((score - 3.14).abs() < 1e-10);
1853 assert_eq!(pos, data.len());
1854 }
1855
1856 #[test]
1858 fn inline_sequential_decode_with_nulls() {
1859 let data = make_data_row(&[
1860 Some(&42i32.to_be_bytes()),
1861 None, Some(b"text"),
1863 ]);
1864
1865 let mut pos: usize = 2;
1866
1867 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1869 pos += 4;
1870 let id = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1871 pos += len as usize;
1872 assert_eq!(id, 42);
1873
1874 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1876 pos += 4;
1877 let name: Option<&str> = if len < 0 {
1878 None
1879 } else {
1880 let s = std::str::from_utf8(&data[pos..pos + len as usize]).unwrap();
1881 pos += len as usize;
1882 Some(s)
1883 };
1884 assert!(name.is_none());
1885
1886 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1888 pos += 4;
1889 let txt = std::str::from_utf8(&data[pos..pos + len as usize]).unwrap();
1890 pos += len as usize;
1891 assert_eq!(txt, "text");
1892 assert_eq!(pos, data.len());
1893 }
1894
1895 #[test]
1897 fn inline_sequential_decode_all_scalar_types() {
1898 let data = make_data_row(&[
1899 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()), ]);
1906
1907 let mut pos: usize = 2;
1908
1909 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1911 pos += 4;
1912 let v_bool = data[pos] != 0;
1913 pos += len as usize;
1914 assert!(v_bool);
1915
1916 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1918 pos += 4;
1919 let v_i16 = i16::from_be_bytes([data[pos], data[pos + 1]]);
1920 pos += len as usize;
1921 assert_eq!(v_i16, 7);
1922
1923 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1925 pos += 4;
1926 let v_i32 = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1927 pos += len as usize;
1928 assert_eq!(v_i32, 42);
1929
1930 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1932 pos += 4;
1933 let v_i64 = i64::from_be_bytes([
1934 data[pos],
1935 data[pos + 1],
1936 data[pos + 2],
1937 data[pos + 3],
1938 data[pos + 4],
1939 data[pos + 5],
1940 data[pos + 6],
1941 data[pos + 7],
1942 ]);
1943 pos += len as usize;
1944 assert_eq!(v_i64, 12345);
1945
1946 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1948 pos += 4;
1949 let v_f32 = f32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1950 pos += len as usize;
1951 assert!((v_f32 - 2.5).abs() < 1e-6);
1952
1953 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1955 pos += 4;
1956 let v_f64 = f64::from_be_bytes([
1957 data[pos],
1958 data[pos + 1],
1959 data[pos + 2],
1960 data[pos + 3],
1961 data[pos + 4],
1962 data[pos + 5],
1963 data[pos + 6],
1964 data[pos + 7],
1965 ]);
1966 pos += len as usize;
1967 assert!((v_f64 - 3.14).abs() < 1e-10);
1968 assert_eq!(pos, data.len());
1969 }
1970
1971 #[test]
1973 fn pg_data_row_new_is_public() {
1974 let data = make_data_row(&[Some(&42i32.to_be_bytes())]);
1975 let row = PgDataRow::new(&data).unwrap();
1977 assert_eq!(row.get_i32(0), Some(42));
1978 }
1979
1980 #[test]
1982 fn inline_decode_matches_pgdatarow() {
1983 let data = make_data_row(&[
1984 Some(&99i32.to_be_bytes()),
1985 Some(b"hello world"),
1986 None,
1987 Some(&[0u8]),
1988 Some(&1.23f64.to_be_bytes()),
1989 ]);
1990
1991 let row = PgDataRow::new(&data).unwrap();
1993 let dr_i32 = row.get_i32(0);
1994 let dr_str = row.get_str(1);
1995 let dr_null = row.get_str(2);
1996 let dr_bool = row.get_bool(3);
1997 let dr_f64 = row.get_f64(4);
1998
1999 let mut pos: usize = 2;
2001
2002 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
2003 pos += 4;
2004 let in_i32 = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
2005 pos += len as usize;
2006
2007 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
2008 pos += 4;
2009 let in_str = std::str::from_utf8(&data[pos..pos + len as usize]).unwrap();
2010 pos += len as usize;
2011
2012 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
2013 pos += 4;
2014 let in_null: Option<&str> = if len < 0 { None } else { unreachable!() };
2015
2016 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
2017 pos += 4;
2018 let in_bool = data[pos] != 0;
2019 pos += len as usize;
2020
2021 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
2022 pos += 4;
2023 let in_f64 = f64::from_be_bytes([
2024 data[pos],
2025 data[pos + 1],
2026 data[pos + 2],
2027 data[pos + 3],
2028 data[pos + 4],
2029 data[pos + 5],
2030 data[pos + 6],
2031 data[pos + 7],
2032 ]);
2033 pos += len as usize;
2034
2035 assert_eq!(dr_i32, Some(in_i32));
2037 assert_eq!(dr_str, Some(in_str));
2038 assert_eq!(dr_null, in_null);
2039 assert_eq!(dr_bool, Some(in_bool));
2040 assert!((dr_f64.unwrap() - in_f64).abs() < 1e-15);
2041 assert_eq!(pos, data.len());
2042 }
2043
2044 #[test]
2049 fn pg_data_row_all_null_columns() {
2050 let data = make_data_row(&[None, None, None, None, None]);
2051 let row = PgDataRow::new(&data).unwrap();
2052 assert_eq!(row.column_count(), 5);
2053 for i in 0..5 {
2054 assert!(row.is_null(i), "column {i} should be null");
2055 assert_eq!(row.get_raw(i), None);
2056 assert_eq!(row.get_i32(i), None);
2057 assert_eq!(row.get_i64(i), None);
2058 assert_eq!(row.get_str(i), None);
2059 assert_eq!(row.get_bool(i), None);
2060 assert_eq!(row.get_f64(i), None);
2061 }
2062 }
2063
2064 #[test]
2065 fn pg_data_row_very_long_text() {
2066 let long_text = "x".repeat(2048);
2067 let data = make_data_row(&[Some(long_text.as_bytes())]);
2068 let row = PgDataRow::new(&data).unwrap();
2069 assert_eq!(row.get_str(0), Some(long_text.as_str()));
2070 }
2071
2072 #[test]
2073 fn pg_data_row_empty_text() {
2074 let data = make_data_row(&[Some(b"")]);
2075 let row = PgDataRow::new(&data).unwrap();
2076 assert!(!row.is_null(0));
2077 assert_eq!(row.get_str(0), Some(""));
2078 assert_eq!(row.get_bytes(0), Some(&[][..]));
2079 }
2080
2081 #[test]
2082 fn pg_data_row_20_columns_exceeds_inline() {
2083 let col_data: Vec<[u8; 4]> = (0..20).map(|i: i32| i.to_be_bytes()).collect();
2084 let cols: Vec<Option<&[u8]>> = col_data.iter().map(|b| Some(b.as_slice())).collect();
2085 let data = make_data_row(&cols);
2086 let row = PgDataRow::new(&data).unwrap();
2087 assert_eq!(row.column_count(), 20);
2088 for i in 0..20 {
2089 assert_eq!(row.get_i32(i), Some(i as i32));
2090 }
2091 }
2092
2093 #[test]
2094 fn pg_data_row_is_null_each_position() {
2095 let data = make_data_row(&[Some(&1i32.to_be_bytes()), None, Some(&3i32.to_be_bytes())]);
2097 let row = PgDataRow::new(&data).unwrap();
2098 assert!(!row.is_null(0));
2099 assert!(row.is_null(1));
2100 assert!(!row.is_null(2));
2101 }
2102
2103 #[test]
2104 fn pg_data_row_negative_column_count() {
2105 let data = (-1i16).to_be_bytes();
2106 assert!(PgDataRow::new(&data).is_err());
2107 }
2108
2109 #[test]
2110 fn pg_data_row_get_str_invalid_utf8() {
2111 let invalid_utf8 = &[0xFF, 0xFE, 0x80];
2112 let data = make_data_row(&[Some(invalid_utf8)]);
2113 let row = PgDataRow::new(&data).unwrap();
2114 assert_eq!(row.get_str(0), None);
2116 assert_eq!(row.get_bytes(0), Some(&[0xFF, 0xFE, 0x80][..]));
2117 }
2118
2119 #[test]
2120 fn pg_data_row_get_i32_wrong_length() {
2121 let data = make_data_row(&[Some(&7i16.to_be_bytes())]);
2123 let row = PgDataRow::new(&data).unwrap();
2124 assert_eq!(row.get_i32(0), None); assert_eq!(row.get_i16(0), Some(7)); }
2127
2128 #[test]
2129 fn pg_data_row_get_i64_wrong_length() {
2130 let data = make_data_row(&[Some(&42i32.to_be_bytes())]);
2132 let row = PgDataRow::new(&data).unwrap();
2133 assert_eq!(row.get_i64(0), None);
2134 }
2135
2136 #[test]
2137 fn pg_data_row_get_f64_wrong_length() {
2138 let data = make_data_row(&[Some(&2.5f32.to_be_bytes())]);
2139 let row = PgDataRow::new(&data).unwrap();
2140 assert_eq!(row.get_f64(0), None); }
2142
2143 #[test]
2144 fn pg_data_row_get_f32_wrong_length() {
2145 let data = make_data_row(&[Some(&3.14f64.to_be_bytes())]);
2146 let row = PgDataRow::new(&data).unwrap();
2147 assert_eq!(row.get_f32(0), None); }
2149
2150 #[test]
2151 fn pg_data_row_get_bool_wrong_length() {
2152 let data = make_data_row(&[Some(&42i32.to_be_bytes())]);
2154 let row = PgDataRow::new(&data).unwrap();
2155 assert_eq!(row.get_bool(0), None);
2156 }
2157
2158 #[test]
2159 fn pg_data_row_unicode_text() {
2160 let texts = [
2161 "\u{1F600}\u{1F4A9}\u{1F680}", "\u{4e16}\u{754c}", "\u{0645}\u{0631}\u{062D}", "\u{1F468}\u{200D}\u{1F469}", ];
2166 for text in &texts {
2167 let data = make_data_row(&[Some(text.as_bytes())]);
2168 let row = PgDataRow::new(&data).unwrap();
2169 assert_eq!(row.get_str(0), Some(*text));
2170 }
2171 }
2172
2173 #[test]
2174 fn pg_data_row_i32_boundary_values() {
2175 for &val in &[i32::MIN, -1, 0, 1, i32::MAX] {
2176 let data = make_data_row(&[Some(&val.to_be_bytes())]);
2177 let row = PgDataRow::new(&data).unwrap();
2178 assert_eq!(row.get_i32(0), Some(val), "failed for {val}");
2179 }
2180 }
2181
2182 #[test]
2183 fn pg_data_row_i64_boundary_values() {
2184 for &val in &[i64::MIN, -1, 0, 1, i64::MAX] {
2185 let data = make_data_row(&[Some(&val.to_be_bytes())]);
2186 let row = PgDataRow::new(&data).unwrap();
2187 assert_eq!(row.get_i64(0), Some(val), "failed for {val}");
2188 }
2189 }
2190
2191 #[test]
2192 fn pg_data_row_f64_special_values() {
2193 let data = make_data_row(&[Some(&f64::INFINITY.to_be_bytes())]);
2194 let row = PgDataRow::new(&data).unwrap();
2195 assert_eq!(row.get_f64(0), Some(f64::INFINITY));
2196
2197 let data = make_data_row(&[Some(&f64::NEG_INFINITY.to_be_bytes())]);
2198 let row = PgDataRow::new(&data).unwrap();
2199 assert_eq!(row.get_f64(0), Some(f64::NEG_INFINITY));
2200
2201 let data = make_data_row(&[Some(&f64::NAN.to_be_bytes())]);
2202 let row = PgDataRow::new(&data).unwrap();
2203 assert!(row.get_f64(0).unwrap().is_nan());
2204 }
2205
2206 #[test]
2207 fn pg_data_row_f32_special_values() {
2208 let data = make_data_row(&[Some(&f32::INFINITY.to_be_bytes())]);
2209 let row = PgDataRow::new(&data).unwrap();
2210 assert_eq!(row.get_f32(0), Some(f32::INFINITY));
2211
2212 let data = make_data_row(&[Some(&f32::NAN.to_be_bytes())]);
2213 let row = PgDataRow::new(&data).unwrap();
2214 assert!(row.get_f32(0).unwrap().is_nan());
2215 }
2216
2217 #[test]
2218 fn pg_data_row_i16_boundary_values() {
2219 for &val in &[i16::MIN, -1, 0, 1, i16::MAX] {
2220 let data = make_data_row(&[Some(&val.to_be_bytes())]);
2221 let row = PgDataRow::new(&data).unwrap();
2222 assert_eq!(row.get_i16(0), Some(val));
2223 }
2224 }
2225
2226 mod proptest_fuzz {
2227 use super::*;
2228 use proptest::prelude::*;
2229
2230 proptest! {
2231 #[test]
2232 fn config_from_url_never_panics(url in ".*") {
2233 let _ = Config::from_url(&url);
2234 }
2235
2236 #[test]
2237 fn url_decode_never_panics(s in ".*") {
2238 let _ = url_decode(&s);
2239 }
2240
2241 #[test]
2242 fn pg_data_row_new_never_panics(data in proptest::collection::vec(any::<u8>(), 0..8192)) {
2243 let _ = PgDataRow::new(&data);
2244 }
2245
2246 #[test]
2247 fn hash_sql_never_panics(sql in ".*") {
2248 let _ = hash_sql(&sql);
2249 }
2250 }
2251 }
2252}