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> {
101 let url = url
102 .strip_prefix("postgres://")
103 .or_else(|| url.strip_prefix("postgresql://"))
104 .ok_or_else(|| DriverError::Protocol("URL must start with postgres://".into()))?;
105
106 let (userinfo, rest) = url
108 .split_once('@')
109 .ok_or_else(|| DriverError::Protocol("missing @ in connection URL".into()))?;
110
111 let (user, password) = userinfo.split_once(':').unwrap_or((userinfo, ""));
112
113 let (hostport, rest) = rest.split_once('/').unwrap_or((rest, ""));
115 let (database, params) = rest.split_once('?').unwrap_or((rest, ""));
116
117 let (host, port) = if let Some((h, p)) = hostport.split_once(':') {
118 let port = p
119 .parse::<u16>()
120 .map_err(|_| DriverError::Protocol(format!("invalid port: {p}")))?;
121 (h.to_owned(), port)
122 } else {
123 (hostport.to_owned(), 5432)
124 };
125
126 let mut ssl = SslMode::Prefer;
127 let mut statement_timeout_secs: u32 = 30;
128 let mut statement_cache_mode = StatementCacheMode::Named;
129 let mut host_override: Option<String> = None;
130 let mut ssl_root_cert: Option<String> = None;
131 let mut ssl_cert: Option<String> = None;
132 let mut ssl_key: Option<String> = None;
133 for param in params.split('&') {
134 if param.is_empty() {
135 continue;
136 }
137 if let Some(val) = param.strip_prefix("sslmode=") {
138 ssl = match val {
140 "disable" => SslMode::Disable,
141 "prefer" => SslMode::Prefer,
142 "require" => SslMode::Require,
143 _ => {
144 return Err(DriverError::Protocol(format!(
145 "unknown sslmode: '{val}' (expected: disable, prefer, require)"
146 )));
147 }
148 };
149 } else if let Some(val) = param.strip_prefix("statement_timeout=") {
150 statement_timeout_secs = val.parse::<u32>().unwrap_or(30);
151 } else if let Some(val) = param.strip_prefix("statement_cache=") {
152 statement_cache_mode = match val {
153 "named" => StatementCacheMode::Named,
154 "disabled" => StatementCacheMode::Disabled,
155 _ => {
156 return Err(DriverError::Protocol(format!(
157 "unknown statement_cache mode: '{val}' (expected: named, disabled)"
158 )));
159 }
160 };
161 } else if let Some(val) = param.strip_prefix("host=") {
162 host_override = Some(url_decode(val)?);
163 } else if let Some(val) = param.strip_prefix("sslrootcert=") {
164 ssl_root_cert = Some(url_decode(val)?);
165 } else if let Some(val) = param.strip_prefix("sslcert=") {
166 ssl_cert = Some(url_decode(val)?);
167 } else if let Some(val) = param.strip_prefix("sslkey=") {
168 ssl_key = Some(url_decode(val)?);
169 }
170 }
171
172 let final_host = if let Some(h) = host_override {
175 h
176 } else {
177 url_decode(&host)?
178 };
179
180 let config = Config {
181 host: final_host,
182 port,
183 user: url_decode(user)?,
184 password: url_decode(password)?,
185 database: if database.is_empty() {
186 url_decode(user)?
187 } else {
188 url_decode(database)?
189 },
190 ssl,
191 statement_timeout_secs,
192 statement_cache_mode,
193 ssl_root_cert,
194 ssl_cert,
195 ssl_key,
196 };
197 config.validate()?;
198 Ok(config)
199 }
200
201 pub fn validate(&self) -> Result<(), DriverError> {
206 if self.host.is_empty() {
207 return Err(DriverError::Protocol("host cannot be empty".into()));
208 }
209 if self.user.is_empty() {
210 return Err(DriverError::Protocol("user cannot be empty".into()));
211 }
212 if self.database.is_empty() {
213 return Err(DriverError::Protocol("database cannot be empty".into()));
214 }
215 Ok(())
216 }
217
218 pub fn host_is_uds(&self) -> bool {
223 self.host.starts_with('/')
224 }
225
226 pub fn uds_path(&self) -> String {
230 format!("{}/.s.PGSQL.{}", self.host, self.port)
231 }
232}
233
234fn url_decode(s: &str) -> Result<String, DriverError> {
244 let mut bytes = Vec::with_capacity(s.len());
245 let input = s.as_bytes();
246 let mut i = 0;
247 while i < input.len() {
248 if input[i] == b'%' {
249 if i + 2 >= input.len() {
250 return Err(DriverError::Protocol(format!(
251 "malformed percent-encoding in URL: '{s}'"
252 )));
253 }
254 let hi = hex_val(input[i + 1]).ok_or_else(|| {
255 DriverError::Protocol(format!(
256 "invalid hex digit '{}' in URL: '{s}'",
257 input[i + 1] as char
258 ))
259 })?;
260 let lo = hex_val(input[i + 2]).ok_or_else(|| {
261 DriverError::Protocol(format!(
262 "invalid hex digit '{}' in URL: '{s}'",
263 input[i + 2] as char
264 ))
265 })?;
266 bytes.push(hi * 16 + lo);
267 i += 3;
268 } else {
269 bytes.push(input[i]);
270 i += 1;
271 }
272 }
273 String::from_utf8(bytes)
274 .map_err(|_| DriverError::Protocol(format!("invalid UTF-8 in URL: '{s}'")))
275}
276
277fn hex_val(b: u8) -> Option<u8> {
278 match b {
279 b'0'..=b'9' => Some(b - b'0'),
280 b'a'..=b'f' => Some(b - b'a' + 10),
281 b'A'..=b'F' => Some(b - b'A' + 10),
282 _ => None,
283 }
284}
285
286pub(crate) enum StartupAction {
292 AuthOk,
293 AuthCleartext,
294 AuthMd5([u8; 4]),
295 AuthSasl(Vec<u8>),
296 ParameterStatus(Box<str>, Box<str>),
297 BackendKeyData(i32, i32),
298 ReadyForQuery(u8),
299 Error(String),
300 Notice,
301}
302
303#[derive(Debug, Clone)]
309pub struct ColumnDesc {
310 pub name: Box<str>,
312 pub type_oid: u32,
314 pub table_oid: u32,
316 pub type_size: i16,
318 pub column_id: i16,
320}
321
322#[derive(Debug, Clone)]
325pub struct PrepareResult {
326 pub columns: Vec<ColumnDesc>,
328 pub param_oids: Vec<u32>,
330}
331
332pub type SimpleRow = Vec<Option<String>>;
337
338#[derive(Debug, Clone)]
344pub struct Notification {
345 pub pid: i32,
347 pub channel: String,
349 pub payload: String,
351}
352
353pub struct QueryResult {
370 pub(crate) all_col_offsets: Vec<(usize, i32)>,
374 pub(crate) num_cols: usize,
376 pub(crate) columns: Arc<[ColumnDesc]>,
377 pub(crate) affected_rows: u64,
378 pub(crate) data_buf: Option<Vec<u8>>,
382}
383
384impl QueryResult {
385 pub fn from_parts(
389 all_col_offsets: Vec<(usize, i32)>,
390 num_cols: usize,
391 columns: Arc<[ColumnDesc]>,
392 affected_rows: u64,
393 ) -> Self {
394 Self {
395 all_col_offsets,
396 num_cols,
397 columns,
398 affected_rows,
399 data_buf: None,
400 }
401 }
402
403 pub fn from_parts_with_buf(
405 all_col_offsets: Vec<(usize, i32)>,
406 num_cols: usize,
407 columns: Arc<[ColumnDesc]>,
408 affected_rows: u64,
409 data_buf: Vec<u8>,
410 ) -> Self {
411 Self {
412 all_col_offsets,
413 num_cols,
414 columns,
415 affected_rows,
416 data_buf: if data_buf.is_empty() {
417 None
418 } else {
419 Some(data_buf)
420 },
421 }
422 }
423
424 pub fn len(&self) -> usize {
426 if self.num_cols == 0 {
427 return 0;
428 }
429 self.all_col_offsets.len() / self.num_cols
430 }
431
432 pub fn is_empty(&self) -> bool {
434 self.all_col_offsets.is_empty()
435 }
436
437 pub fn affected_rows(&self) -> u64 {
439 self.affected_rows
440 }
441
442 pub fn columns(&self) -> &[ColumnDesc] {
444 &self.columns
445 }
446
447 pub fn row<'a>(&'a self, idx: usize, arena: &'a Arena) -> Row<'a> {
450 let start = idx * self.num_cols;
451 let end = start + self.num_cols;
452 Row {
453 data: self.data_buf.as_deref(),
454 arena,
455 col_offsets: &self.all_col_offsets[start..end],
456 columns: &self.columns,
457 }
458 }
459
460 pub fn take_col_offsets(&mut self) -> Vec<(usize, i32)> {
465 std::mem::take(&mut self.all_col_offsets)
466 }
467
468 pub fn take_data_buf(&mut self) -> Option<Vec<u8>> {
470 self.data_buf.take()
471 }
472
473 pub fn rows<'a>(&'a self, arena: &'a Arena) -> impl Iterator<Item = Row<'a>> {
475 let num_cols = self.num_cols;
476 let columns = &self.columns;
477 let data = self.data_buf.as_deref();
478 self.all_col_offsets
479 .chunks(num_cols.max(1))
480 .map(move |chunk| Row {
481 data,
482 arena,
483 col_offsets: chunk,
484 columns,
485 })
486 }
487}
488
489pub struct Row<'a> {
500 data: Option<&'a [u8]>,
503 arena: &'a Arena,
504 col_offsets: &'a [(usize, i32)],
505 columns: &'a [ColumnDesc],
506}
507
508impl<'a> Row<'a> {
509 #[inline]
511 pub fn get_raw(&self, idx: usize) -> Option<&'a [u8]> {
512 let (offset, len) = self.col_offsets[idx];
513 if len < 0 {
514 None
515 } else if let Some(buf) = self.data {
516 Some(&buf[offset..offset + len as usize])
517 } else {
518 Some(self.arena.get(offset, len as usize))
519 }
520 }
521
522 #[inline]
524 pub fn is_null(&self, idx: usize) -> bool {
525 self.col_offsets[idx].1 < 0
526 }
527
528 #[inline]
530 pub fn column_count(&self) -> usize {
531 self.col_offsets.len()
532 }
533
534 #[inline]
536 pub fn get_bool(&self, idx: usize) -> Option<bool> {
537 self.get_raw(idx)
538 .and_then(|data| crate::codec::decode_bool(data).ok())
539 }
540
541 #[inline]
543 pub fn get_i16(&self, idx: usize) -> Option<i16> {
544 self.get_raw(idx)
545 .and_then(|data| crate::codec::decode_i16(data).ok())
546 }
547
548 #[inline]
550 pub fn get_i32(&self, idx: usize) -> Option<i32> {
551 self.get_raw(idx)
552 .and_then(|data| crate::codec::decode_i32(data).ok())
553 }
554
555 #[inline]
557 pub fn get_i64(&self, idx: usize) -> Option<i64> {
558 self.get_raw(idx)
559 .and_then(|data| crate::codec::decode_i64(data).ok())
560 }
561
562 #[inline]
564 pub fn get_f32(&self, idx: usize) -> Option<f32> {
565 self.get_raw(idx)
566 .and_then(|data| crate::codec::decode_f32(data).ok())
567 }
568
569 #[inline]
571 pub fn get_f64(&self, idx: usize) -> Option<f64> {
572 self.get_raw(idx)
573 .and_then(|data| crate::codec::decode_f64(data).ok())
574 }
575
576 #[inline]
578 pub fn get_str(&self, idx: usize) -> Option<&'a str> {
579 self.get_raw(idx)
580 .and_then(|data| crate::codec::decode_str(data).ok())
581 }
582
583 #[inline]
585 pub fn get_bytes(&self, idx: usize) -> Option<&'a [u8]> {
586 self.get_raw(idx)
587 }
588
589 #[inline]
591 pub fn column_name(&self, idx: usize) -> &str {
592 &self.columns[idx].name
593 }
594
595 #[inline]
597 pub fn column_type_oid(&self, idx: usize) -> u32 {
598 self.columns[idx].type_oid
599 }
600}
601
602pub struct PgDataRow<'a> {
615 data: &'a [u8],
616 offsets: smallvec::SmallVec<[(usize, i32); 16]>,
619}
620
621impl<'a> PgDataRow<'a> {
622 pub fn new(data: &'a [u8]) -> Result<Self, DriverError> {
627 if data.len() < 2 {
628 return Err(DriverError::Protocol("DataRow too short".into()));
629 }
630 let num_cols = i16::from_be_bytes([data[0], data[1]]);
631 if num_cols < 0 {
632 return Err(DriverError::Protocol(
633 "DataRow: negative column count".into(),
634 ));
635 }
636 let num_cols = num_cols as usize;
637 let mut offsets = smallvec::SmallVec::<[(usize, i32); 16]>::with_capacity(num_cols);
638 let mut pos = 2usize;
639 for _ in 0..num_cols {
640 if pos + 4 > data.len() {
641 return Err(DriverError::Protocol("DataRow truncated".into()));
642 }
643 let col_len =
644 i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
645 pos += 4;
646 offsets.push((pos, col_len));
647 if col_len > 0 {
648 pos += col_len as usize;
649 }
650 }
651 Ok(Self { data, offsets })
652 }
653
654 #[inline]
656 pub fn get_raw(&self, idx: usize) -> Option<&'a [u8]> {
657 let (offset, len) = self.offsets[idx];
658 if len < 0 {
659 None
660 } else {
661 Some(&self.data[offset..offset + len as usize])
662 }
663 }
664
665 #[inline]
667 pub fn is_null(&self, idx: usize) -> bool {
668 self.offsets[idx].1 < 0
669 }
670
671 #[inline]
673 pub fn column_count(&self) -> usize {
674 self.offsets.len()
675 }
676
677 #[inline]
679 pub fn get_bool(&self, idx: usize) -> Option<bool> {
680 self.get_raw(idx)
681 .and_then(|data| crate::codec::decode_bool(data).ok())
682 }
683
684 #[inline]
686 pub fn get_i16(&self, idx: usize) -> Option<i16> {
687 self.get_raw(idx)
688 .and_then(|data| crate::codec::decode_i16(data).ok())
689 }
690
691 #[inline]
693 pub fn get_i32(&self, idx: usize) -> Option<i32> {
694 self.get_raw(idx)
695 .and_then(|data| crate::codec::decode_i32(data).ok())
696 }
697
698 #[inline]
700 pub fn get_i64(&self, idx: usize) -> Option<i64> {
701 self.get_raw(idx)
702 .and_then(|data| crate::codec::decode_i64(data).ok())
703 }
704
705 #[inline]
707 pub fn get_f32(&self, idx: usize) -> Option<f32> {
708 self.get_raw(idx)
709 .and_then(|data| crate::codec::decode_f32(data).ok())
710 }
711
712 #[inline]
714 pub fn get_f64(&self, idx: usize) -> Option<f64> {
715 self.get_raw(idx)
716 .and_then(|data| crate::codec::decode_f64(data).ok())
717 }
718
719 #[inline]
721 pub fn get_str(&self, idx: usize) -> Option<&'a str> {
722 self.get_raw(idx)
723 .and_then(|data| crate::codec::decode_str(data).ok())
724 }
725
726 #[inline]
728 pub fn get_bytes(&self, idx: usize) -> Option<&'a [u8]> {
729 self.get_raw(idx)
730 }
731}
732
733pub fn hash_sql(sql: &str) -> u64 {
741 use std::hash::{Hash, Hasher};
742 let mut hasher = RapidHasher::default();
743 sql.hash(&mut hasher);
744 hasher.finish()
745}
746
747#[cfg(test)]
752#[allow(clippy::approx_constant)]
753mod tests {
754 use super::*;
755
756 #[test]
761 fn config_parse_full_url() {
762 let cfg = Config::from_url("postgres://user:pass@localhost:5432/mydb").unwrap();
763 assert_eq!(cfg.user, "user");
764 assert_eq!(cfg.password, "pass");
765 assert_eq!(cfg.host, "localhost");
766 assert_eq!(cfg.port, 5432);
767 assert_eq!(cfg.database, "mydb");
768 }
769
770 #[test]
771 fn config_parse_default_port() {
772 let cfg = Config::from_url("postgres://user:pass@localhost/mydb").unwrap();
773 assert_eq!(cfg.port, 5432);
774 }
775
776 #[test]
777 fn config_parse_no_password() {
778 let cfg = Config::from_url("postgres://user@localhost/mydb").unwrap();
779 assert_eq!(cfg.user, "user");
780 assert_eq!(cfg.password, "");
781 }
782
783 #[test]
784 fn config_parse_empty_database() {
785 let cfg = Config::from_url("postgres://user:pass@localhost").unwrap();
786 assert_eq!(cfg.database, "user");
788 }
789
790 #[test]
791 fn config_parse_sslmode() {
792 let cfg = Config::from_url("postgres://user:pass@localhost/db?sslmode=require").unwrap();
793 assert_eq!(cfg.ssl, SslMode::Require);
794 }
795
796 #[test]
797 fn config_parse_percent_encoding() {
798 let cfg = Config::from_url("postgres://user%40domain:p%40ss@localhost/db").unwrap();
799 assert_eq!(cfg.user, "user@domain");
800 assert_eq!(cfg.password, "p@ss");
801 }
802
803 #[test]
804 fn config_rejects_bad_scheme() {
805 let result = Config::from_url("mysql://user:pass@localhost/db");
806 assert!(result.is_err());
807 }
808
809 #[test]
811 fn config_rejects_unknown_sslmode() {
812 let result = Config::from_url("postgres://user:pass@localhost/db?sslmode=requre");
813 assert!(result.is_err(), "typo 'requre' should be rejected");
814 let result = Config::from_url("postgres://user:pass@localhost/db?sslmode=REQUIRE");
815 assert!(result.is_err(), "uppercase should be rejected");
816 let result = Config::from_url("postgres://user:pass@localhost/db?sslmode=bogus");
817 assert!(result.is_err(), "bogus value should be rejected");
818 }
819
820 #[test]
822 fn config_accepts_valid_sslmodes() {
823 let cfg = Config::from_url("postgres://user:pass@localhost/db?sslmode=disable").unwrap();
824 assert_eq!(cfg.ssl, SslMode::Disable);
825 let cfg = Config::from_url("postgres://user:pass@localhost/db?sslmode=prefer").unwrap();
826 assert_eq!(cfg.ssl, SslMode::Prefer);
827 let cfg = Config::from_url("postgres://user:pass@localhost/db?sslmode=require").unwrap();
828 assert_eq!(cfg.ssl, SslMode::Require);
829 }
830
831 #[test]
833 fn config_parse_postgresql_scheme() {
834 let cfg = Config::from_url("postgresql://user:pass@localhost:5432/mydb").unwrap();
835 assert_eq!(cfg.user, "user");
836 assert_eq!(cfg.password, "pass");
837 assert_eq!(cfg.host, "localhost");
838 assert_eq!(cfg.port, 5432);
839 assert_eq!(cfg.database, "mydb");
840 }
841
842 #[test]
844 fn config_parse_no_password_standalone() {
845 let cfg = Config::from_url("postgres://admin@db.example.com/myapp").unwrap();
846 assert_eq!(cfg.user, "admin");
847 assert_eq!(cfg.password, "");
848 assert_eq!(cfg.host, "db.example.com");
849 assert_eq!(cfg.database, "myapp");
850 }
851
852 #[test]
854 fn config_empty_database_falls_back_to_user() {
855 let cfg = Config::from_url("postgres://testuser:pass@localhost").unwrap();
856 assert_eq!(cfg.database, "testuser");
857 }
858
859 #[test]
861 fn config_unknown_sslmode_error() {
862 let result = Config::from_url("postgres://u:p@h/d?sslmode=verify-full");
863 assert!(result.is_err());
864 let err = result.unwrap_err().to_string();
865 assert!(
866 err.contains("unknown sslmode"),
867 "should describe unknown sslmode: {err}"
868 );
869 }
870
871 #[test]
873 fn config_multiple_query_params() {
874 let cfg = Config::from_url(
875 "postgres://user:pass@localhost/db?sslmode=disable&statement_timeout=60",
876 )
877 .unwrap();
878 assert_eq!(cfg.ssl, SslMode::Disable);
879 assert_eq!(cfg.statement_timeout_secs, 60);
880 }
881
882 #[test]
884 fn config_validate_empty_host() {
885 let cfg = Config {
886 host: String::new(),
887 port: 5432,
888 user: "user".into(),
889 password: "pass".into(),
890 database: "db".into(),
891 ssl: SslMode::Disable,
892 statement_timeout_secs: 30,
893 statement_cache_mode: StatementCacheMode::Named,
894 ssl_root_cert: None,
895 ssl_cert: None,
896 ssl_key: None,
897 };
898 assert!(cfg.validate().is_err());
899 }
900
901 #[test]
903 fn config_validate_empty_user() {
904 let cfg = Config {
905 host: "localhost".into(),
906 port: 5432,
907 user: String::new(),
908 password: "pass".into(),
909 database: "db".into(),
910 ssl: SslMode::Disable,
911 statement_timeout_secs: 30,
912 statement_cache_mode: StatementCacheMode::Named,
913 ssl_root_cert: None,
914 ssl_cert: None,
915 ssl_key: None,
916 };
917 assert!(cfg.validate().is_err());
918 }
919
920 #[test]
922 fn config_validate_empty_database() {
923 let cfg = Config {
924 host: "localhost".into(),
925 port: 5432,
926 user: "user".into(),
927 password: "pass".into(),
928 database: String::new(),
929 ssl: SslMode::Disable,
930 statement_timeout_secs: 30,
931 statement_cache_mode: StatementCacheMode::Named,
932 ssl_root_cert: None,
933 ssl_cert: None,
934 ssl_key: None,
935 };
936 assert!(cfg.validate().is_err());
937 }
938
939 #[test]
941 fn config_missing_at_sign() {
942 let result = Config::from_url("postgres://userpasslocalhost/db");
943 assert!(result.is_err());
944 }
945
946 #[test]
948 fn config_custom_port() {
949 let cfg = Config::from_url("postgres://user:pass@localhost:5433/db").unwrap();
950 assert_eq!(cfg.port, 5433);
951 }
952
953 #[test]
955 fn config_invalid_port() {
956 let result = Config::from_url("postgres://user:pass@localhost:notaport/db");
957 assert!(result.is_err());
958 }
959
960 #[cfg(not(feature = "tls"))]
962 #[test]
963 fn config_sslmode_require_without_tls_feature() {
964 let cfg = Config::from_url("postgres://user:pass@localhost/db?sslmode=require").unwrap();
967 assert_eq!(cfg.ssl, SslMode::Require);
968 }
969
970 #[test]
971 fn config_statement_timeout_default() {
972 let cfg = Config::from_url("postgres://user:pass@localhost/db").unwrap();
973 assert_eq!(cfg.statement_timeout_secs, 30);
974 }
975
976 #[test]
977 fn config_statement_timeout_custom() {
978 let cfg =
979 Config::from_url("postgres://user:pass@localhost/db?statement_timeout=120").unwrap();
980 assert_eq!(cfg.statement_timeout_secs, 120);
981 }
982
983 #[test]
984 fn config_statement_timeout_zero() {
985 let cfg =
986 Config::from_url("postgres://user:pass@localhost/db?statement_timeout=0").unwrap();
987 assert_eq!(cfg.statement_timeout_secs, 0);
988 }
989
990 #[test]
991 fn config_statement_timeout_invalid_falls_back() {
992 let cfg =
993 Config::from_url("postgres://user:pass@localhost/db?statement_timeout=notanumber")
994 .unwrap();
995 assert_eq!(cfg.statement_timeout_secs, 30); }
997
998 #[test]
1003 fn parse_statement_cache_default() {
1004 let cfg = Config::from_url("postgres://user:pass@localhost/db").unwrap();
1005 assert_eq!(cfg.statement_cache_mode, StatementCacheMode::Named);
1006 }
1007
1008 #[test]
1009 fn parse_statement_cache_named() {
1010 let cfg =
1011 Config::from_url("postgres://user:pass@localhost/db?statement_cache=named").unwrap();
1012 assert_eq!(cfg.statement_cache_mode, StatementCacheMode::Named);
1013 }
1014
1015 #[test]
1016 fn parse_statement_cache_disabled() {
1017 let cfg =
1018 Config::from_url("postgres://user:pass@localhost/db?statement_cache=disabled").unwrap();
1019 assert_eq!(cfg.statement_cache_mode, StatementCacheMode::Disabled);
1020 }
1021
1022 #[test]
1023 fn parse_statement_cache_invalid() {
1024 let result = Config::from_url("postgres://user:pass@localhost/db?statement_cache=off");
1025 assert!(result.is_err(), "invalid value 'off' should be rejected");
1026 let result = Config::from_url("postgres://user:pass@localhost/db?statement_cache=DISABLED");
1027 assert!(result.is_err(), "uppercase should be rejected");
1028 let result = Config::from_url("postgres://user:pass@localhost/db?statement_cache=bogus");
1029 assert!(result.is_err(), "bogus value should be rejected");
1030 }
1031
1032 #[test]
1033 fn parse_statement_cache_with_other_params() {
1034 let cfg = Config::from_url(
1035 "postgres://user:pass@localhost/db?sslmode=disable&statement_cache=disabled&statement_timeout=60",
1036 )
1037 .unwrap();
1038 assert_eq!(cfg.statement_cache_mode, StatementCacheMode::Disabled);
1039 assert_eq!(cfg.ssl, SslMode::Disable);
1040 assert_eq!(cfg.statement_timeout_secs, 60);
1041 }
1042
1043 #[test]
1044 fn statement_cache_mode_default_is_named() {
1045 assert_eq!(StatementCacheMode::default(), StatementCacheMode::Named);
1046 }
1047
1048 #[test]
1053 fn parse_ssl_root_cert() {
1054 let cfg = Config::from_url("postgres://user:pass@localhost/db?sslrootcert=/path/to/ca.pem")
1055 .unwrap();
1056 assert_eq!(cfg.ssl_root_cert.as_deref(), Some("/path/to/ca.pem"));
1057 assert_eq!(cfg.ssl_cert, None);
1058 assert_eq!(cfg.ssl_key, None);
1059 }
1060
1061 #[test]
1062 fn parse_ssl_cert_and_key() {
1063 let cfg = Config::from_url(
1064 "postgres://user:pass@localhost/db?sslcert=/path/to/client.pem&sslkey=/path/to/client.key",
1065 )
1066 .unwrap();
1067 assert_eq!(cfg.ssl_root_cert, None);
1068 assert_eq!(cfg.ssl_cert.as_deref(), Some("/path/to/client.pem"));
1069 assert_eq!(cfg.ssl_key.as_deref(), Some("/path/to/client.key"));
1070 }
1071
1072 #[test]
1073 fn parse_ssl_all_tls_params() {
1074 let cfg = Config::from_url(
1075 "postgres://user:pass@localhost/db?sslmode=require&sslrootcert=/ca.pem&sslcert=/client.pem&sslkey=/client.key",
1076 )
1077 .unwrap();
1078 assert_eq!(cfg.ssl, SslMode::Require);
1079 assert_eq!(cfg.ssl_root_cert.as_deref(), Some("/ca.pem"));
1080 assert_eq!(cfg.ssl_cert.as_deref(), Some("/client.pem"));
1081 assert_eq!(cfg.ssl_key.as_deref(), Some("/client.key"));
1082 }
1083
1084 #[test]
1085 fn parse_ssl_paths_percent_encoded() {
1086 let cfg = Config::from_url("postgres://user:pass@localhost/db?sslrootcert=%2Ftmp%2Fca.pem")
1088 .unwrap();
1089 assert_eq!(cfg.ssl_root_cert.as_deref(), Some("/tmp/ca.pem"));
1090 }
1091
1092 #[test]
1093 fn parse_ssl_params_default_none() {
1094 let cfg = Config::from_url("postgres://user:pass@localhost/db").unwrap();
1095 assert_eq!(cfg.ssl_root_cert, None);
1096 assert_eq!(cfg.ssl_cert, None);
1097 assert_eq!(cfg.ssl_key, None);
1098 }
1099
1100 #[test]
1101 fn config_uds_path_format() {
1102 let cfg = Config::from_url("postgres://user@localhost/db?host=/tmp").unwrap();
1103 assert_eq!(cfg.uds_path(), "/tmp/.s.PGSQL.5432");
1104 }
1105
1106 #[test]
1107 fn config_uds_path_custom_port() {
1108 let cfg = Config::from_url("postgres://user@localhost:5433/db?host=/tmp").unwrap();
1109 assert_eq!(cfg.uds_path(), "/tmp/.s.PGSQL.5433");
1110 }
1111
1112 #[test]
1117 fn config_host_is_uds_absolute_path() {
1118 let cfg = Config {
1119 host: "/tmp".into(),
1120 port: 5432,
1121 user: "user".into(),
1122 password: "".into(),
1123 database: "db".into(),
1124 ssl: SslMode::Disable,
1125 statement_timeout_secs: 30,
1126 statement_cache_mode: StatementCacheMode::Named,
1127 ssl_root_cert: None,
1128 ssl_cert: None,
1129 ssl_key: None,
1130 };
1131 assert!(cfg.host_is_uds());
1132 assert_eq!(cfg.uds_path(), "/tmp/.s.PGSQL.5432");
1133 }
1134
1135 #[test]
1136 fn config_host_is_uds_var_run() {
1137 let cfg = Config {
1138 host: "/var/run/postgresql".into(),
1139 port: 5433,
1140 user: "user".into(),
1141 password: "".into(),
1142 database: "db".into(),
1143 ssl: SslMode::Disable,
1144 statement_timeout_secs: 30,
1145 statement_cache_mode: StatementCacheMode::Named,
1146 ssl_root_cert: None,
1147 ssl_cert: None,
1148 ssl_key: None,
1149 };
1150 assert!(cfg.host_is_uds());
1151 assert_eq!(cfg.uds_path(), "/var/run/postgresql/.s.PGSQL.5433");
1152 }
1153
1154 #[test]
1155 fn config_host_is_not_uds_for_hostname() {
1156 let cfg = Config {
1157 host: "localhost".into(),
1158 port: 5432,
1159 user: "user".into(),
1160 password: "".into(),
1161 database: "db".into(),
1162 ssl: SslMode::Disable,
1163 statement_timeout_secs: 30,
1164 statement_cache_mode: StatementCacheMode::Named,
1165 ssl_root_cert: None,
1166 ssl_cert: None,
1167 ssl_key: None,
1168 };
1169 assert!(!cfg.host_is_uds());
1170 }
1171
1172 #[test]
1173 fn config_host_is_not_uds_for_ip() {
1174 let cfg = Config {
1175 host: "127.0.0.1".into(),
1176 port: 5432,
1177 user: "user".into(),
1178 password: "".into(),
1179 database: "db".into(),
1180 ssl: SslMode::Disable,
1181 statement_timeout_secs: 30,
1182 statement_cache_mode: StatementCacheMode::Named,
1183 ssl_root_cert: None,
1184 ssl_cert: None,
1185 ssl_key: None,
1186 };
1187 assert!(!cfg.host_is_uds());
1188 }
1189
1190 #[test]
1191 fn config_parse_uds_host_query_param() {
1192 let cfg = Config::from_url("postgres://user@localhost/mydb?host=/tmp").unwrap();
1193 assert_eq!(cfg.host, "/tmp");
1194 assert!(cfg.host_is_uds());
1195 assert_eq!(cfg.uds_path(), "/tmp/.s.PGSQL.5432");
1196 assert_eq!(cfg.database, "mydb");
1197 assert_eq!(cfg.user, "user");
1198 }
1199
1200 #[test]
1201 fn config_parse_uds_host_query_param_custom_port() {
1202 let cfg = Config::from_url("postgres://user@localhost:5433/mydb?host=/var/run/postgresql")
1203 .unwrap();
1204 assert_eq!(cfg.host, "/var/run/postgresql");
1205 assert_eq!(cfg.port, 5433);
1206 assert_eq!(cfg.uds_path(), "/var/run/postgresql/.s.PGSQL.5433");
1207 }
1208
1209 #[test]
1210 fn config_parse_uds_host_with_other_params() {
1211 let cfg = Config::from_url(
1212 "postgres://user@localhost/db?host=/tmp&sslmode=disable&statement_timeout=60",
1213 )
1214 .unwrap();
1215 assert_eq!(cfg.host, "/tmp");
1216 assert!(cfg.host_is_uds());
1217 assert_eq!(cfg.ssl, SslMode::Disable);
1218 assert_eq!(cfg.statement_timeout_secs, 60);
1219 }
1220
1221 #[test]
1222 fn config_parse_uds_host_percent_encoded() {
1223 let cfg = Config::from_url("postgres://user@localhost/db?host=%2Ftmp").unwrap();
1225 assert_eq!(cfg.host, "/tmp");
1226 assert!(cfg.host_is_uds());
1227 }
1228
1229 #[test]
1230 fn config_parse_tcp_host_not_overridden_without_param() {
1231 let cfg = Config::from_url("postgres://user@myserver/db").unwrap();
1233 assert_eq!(cfg.host, "myserver");
1234 assert!(!cfg.host_is_uds());
1235 }
1236
1237 #[test]
1238 fn config_parse_uds_host_overrides_url_hostname() {
1239 let cfg = Config::from_url("postgres://user@db.example.com/mydb?host=/var/run/postgresql")
1241 .unwrap();
1242 assert_eq!(cfg.host, "/var/run/postgresql");
1243 assert!(cfg.host_is_uds());
1244 }
1245
1246 #[test]
1247 fn config_parse_uds_empty_url_host() {
1248 let cfg = Config::from_url("postgres://user@/mydb?host=/tmp").unwrap();
1250 assert_eq!(cfg.host, "/tmp");
1251 assert!(cfg.host_is_uds());
1252 assert_eq!(cfg.database, "mydb");
1253 }
1254
1255 #[test]
1260 fn url_decode_works() {
1261 assert_eq!(url_decode("hello%20world").unwrap(), "hello world");
1262 assert_eq!(url_decode("no%20escape").unwrap(), "no escape");
1263 assert_eq!(url_decode("plain").unwrap(), "plain");
1264 assert_eq!(url_decode("a%40b").unwrap(), "a@b");
1265 }
1266
1267 #[test]
1268 fn url_decode_malformed_percent_trailing() {
1269 let result = url_decode("abc%2");
1271 assert!(result.is_err(), "truncated %2 should error");
1272 }
1273
1274 #[test]
1275 fn url_decode_malformed_percent_no_digits() {
1276 let result = url_decode("abc%");
1278 assert!(result.is_err(), "bare % at end should error");
1279 }
1280
1281 #[test]
1282 fn url_decode_invalid_hex_digit() {
1283 let result = url_decode("abc%GG");
1285 assert!(result.is_err(), "%GG should error");
1286 }
1287
1288 #[test]
1289 fn url_decode_invalid_hex_second_digit() {
1290 let result = url_decode("abc%2Z");
1292 assert!(result.is_err(), "%2Z should error");
1293 }
1294
1295 #[test]
1297 fn url_decode_invalid_utf8_percent() {
1298 let result = url_decode("%80%81");
1300 assert!(result.is_err(), "invalid UTF-8 bytes should error");
1301 }
1302
1303 #[test]
1305 fn url_decode_percent_everywhere() {
1306 assert_eq!(url_decode("%41%42%43").unwrap(), "ABC");
1307 assert_eq!(url_decode("%61").unwrap(), "a");
1308 assert_eq!(url_decode("x%2Fy%2Fz").unwrap(), "x/y/z");
1309 }
1310
1311 #[test]
1313 fn url_decode_bare_percent_middle() {
1314 assert!(url_decode("a%b").is_err(), "bare % in middle should error");
1315 }
1316
1317 #[test]
1319 fn url_decode_multibyte_utf8() {
1320 let result = url_decode("caf%C3%A9").unwrap();
1321 assert_eq!(result, "caf\u{00e9}"); }
1323
1324 #[test]
1326 fn url_decode_invalid_percent_zz() {
1327 let result = url_decode("abc%ZZ");
1328 assert!(result.is_err(), "%ZZ should error");
1329 }
1330
1331 #[test]
1333 fn url_decode_truncated_percent_trailing() {
1334 let result = url_decode("abc%");
1335 assert!(result.is_err(), "trailing % should error");
1336 }
1337
1338 #[test]
1340 fn url_decode_invalid_utf8() {
1341 let result = url_decode("%80");
1343 assert!(result.is_err(), "invalid UTF-8 should error");
1344 }
1345
1346 #[test]
1347 fn url_decode_empty_string() {
1348 assert_eq!(url_decode("").unwrap(), "");
1349 }
1350
1351 #[test]
1352 fn url_decode_no_encoding() {
1353 assert_eq!(url_decode("hello").unwrap(), "hello");
1354 }
1355
1356 #[test]
1357 fn url_decode_all_ascii_hex() {
1358 assert_eq!(url_decode("%2F").unwrap(), "/");
1360 assert_eq!(url_decode("%2f").unwrap(), "/");
1361 }
1362
1363 #[test]
1367 fn config_unicode_password() {
1368 let cfg =
1370 Config::from_url("postgres://user:%D0%BF%D0%B0%D1%80%D0%BE%D0%BB%D1%8C@localhost/db")
1371 .unwrap();
1372 assert_eq!(cfg.user, "user");
1373 assert_eq!(
1374 cfg.password,
1375 "\u{043F}\u{0430}\u{0440}\u{043E}\u{043B}\u{044C}"
1376 ); assert_eq!(cfg.host, "localhost");
1378 assert_eq!(cfg.database, "db");
1379 }
1380
1381 #[test]
1383 fn config_port_zero() {
1384 let cfg = Config::from_url("postgres://user:pass@localhost:0/db").unwrap();
1385 assert_eq!(cfg.port, 0);
1386 }
1387
1388 #[test]
1390 fn config_port_max() {
1391 let cfg = Config::from_url("postgres://user:pass@localhost:65535/db").unwrap();
1392 assert_eq!(cfg.port, 65535);
1393 }
1394
1395 #[test]
1397 fn config_port_overflow() {
1398 let result = Config::from_url("postgres://user:pass@localhost:65536/db");
1399 assert!(result.is_err(), "port 65536 exceeds u16 max");
1400 }
1401
1402 #[test]
1404 fn config_unknown_param_ignored() {
1405 let cfg = Config::from_url(
1406 "postgres://user:pass@localhost/db?application_name=myapp&connect_timeout=10",
1407 )
1408 .unwrap();
1409 assert_eq!(cfg.user, "user");
1411 assert_eq!(cfg.host, "localhost");
1412 assert_eq!(cfg.database, "db");
1413 assert_eq!(cfg.statement_timeout_secs, 30);
1415 assert_eq!(cfg.ssl, SslMode::Prefer);
1416 }
1417
1418 #[test]
1420 fn url_decode_double_percent_encoding() {
1421 assert_eq!(url_decode("%2525").unwrap(), "%25");
1423 }
1424
1425 #[test]
1427 fn config_explicit_empty_password() {
1428 let cfg = Config::from_url("postgres://user:@localhost/db").unwrap();
1429 assert_eq!(cfg.user, "user");
1430 assert_eq!(cfg.password, "");
1431 }
1432
1433 #[test]
1435 fn config_special_chars_in_user() {
1436 let cfg = Config::from_url("postgres://my%2Fuser:pass@localhost/my%2Fdb").unwrap();
1437 assert_eq!(cfg.user, "my/user");
1438 assert_eq!(cfg.database, "my/db");
1439 }
1440
1441 #[test]
1443 fn url_decode_plus_is_literal() {
1444 assert_eq!(url_decode("a+b").unwrap(), "a+b");
1445 }
1446
1447 #[test]
1449 fn config_minimal_valid_url() {
1450 let cfg = Config::from_url("postgres://user@localhost/db").unwrap();
1451 assert_eq!(cfg.user, "user");
1452 assert_eq!(cfg.password, "");
1453 assert_eq!(cfg.host, "localhost");
1454 assert_eq!(cfg.port, 5432);
1455 assert_eq!(cfg.database, "db");
1456 }
1457
1458 #[test]
1460 fn config_empty_param_segments() {
1461 let cfg =
1462 Config::from_url("postgres://user:pass@localhost/db?&&statement_timeout=60&&").unwrap();
1463 assert_eq!(cfg.statement_timeout_secs, 60);
1464 }
1465
1466 #[test]
1471 fn hash_sql_deterministic() {
1472 let h1 = hash_sql("SELECT 1");
1473 let h2 = hash_sql("SELECT 1");
1474 assert_eq!(h1, h2);
1475 }
1476
1477 #[test]
1478 fn hash_sql_different_queries() {
1479 let h1 = hash_sql("SELECT 1");
1480 let h2 = hash_sql("SELECT 2");
1481 assert_ne!(h1, h2);
1482 }
1483
1484 #[test]
1485 fn hash_sql_empty() {
1486 let _h = hash_sql(""); }
1488
1489 #[test]
1490 fn hash_sql_whitespace_only() {
1491 let h = hash_sql(" ");
1492 assert_ne!(h, hash_sql(""));
1493 }
1494
1495 #[test]
1496 fn hash_sql_very_long() {
1497 let long_sql = "SELECT ".to_string() + &"x".repeat(10_000);
1498 let h = hash_sql(&long_sql);
1499 assert_eq!(h, hash_sql(&long_sql));
1500 }
1501
1502 #[test]
1503 fn hash_sql_unicode() {
1504 let h = hash_sql("SELECT '\u{1F600}'");
1505 assert_ne!(h, hash_sql("SELECT 'x'"));
1506 }
1507
1508 #[test]
1513 fn notification_struct_fields() {
1514 let n = Notification {
1515 pid: 42,
1516 channel: "test_chan".to_owned(),
1517 payload: "hello".to_owned(),
1518 };
1519 assert_eq!(n.pid, 42);
1520 assert_eq!(n.channel, "test_chan");
1521 assert_eq!(n.payload, "hello");
1522 }
1523
1524 #[test]
1525 fn notification_clone() {
1526 let n = Notification {
1527 pid: 1,
1528 channel: "c".to_owned(),
1529 payload: "p".to_owned(),
1530 };
1531 let n2 = n.clone();
1532 assert_eq!(n2.pid, 1);
1533 assert_eq!(n2.channel, "c");
1534 }
1535
1536 #[test]
1537 fn notification_debug() {
1538 let n = Notification {
1539 pid: 1,
1540 channel: "c".to_owned(),
1541 payload: "p".to_owned(),
1542 };
1543 let dbg = format!("{n:?}");
1544 assert!(dbg.contains("Notification"));
1545 }
1546
1547 #[test]
1552 fn query_result_empty() {
1553 let result = QueryResult {
1554 all_col_offsets: vec![],
1555 num_cols: 0,
1556 columns: Arc::from(Vec::new()),
1557 affected_rows: 0,
1558 data_buf: None,
1559 };
1560 assert!(result.is_empty());
1561 assert_eq!(result.len(), 0);
1562 }
1563
1564 #[test]
1565 fn query_result_from_parts() {
1566 let result = QueryResult::from_parts(vec![(0, 4), (0, -1)], 2, Arc::from(Vec::new()), 5);
1567 assert_eq!(result.len(), 1);
1568 assert_eq!(result.num_cols, 2);
1569 assert_eq!(result.affected_rows, 5);
1570 }
1571
1572 #[test]
1573 fn query_result_affected_rows() {
1574 let result = QueryResult {
1575 all_col_offsets: vec![],
1576 num_cols: 0,
1577 columns: Arc::from(Vec::new()),
1578 affected_rows: 42,
1579 data_buf: None,
1580 };
1581 assert_eq!(result.affected_rows, 42);
1582 assert!(result.is_empty());
1583 }
1584
1585 fn make_data_row(columns: &[Option<&[u8]>]) -> Vec<u8> {
1592 let mut buf = Vec::new();
1593 buf.extend_from_slice(&(columns.len() as i16).to_be_bytes());
1594 for col in columns {
1595 match col {
1596 Some(data) => {
1597 buf.extend_from_slice(&(data.len() as i32).to_be_bytes());
1598 buf.extend_from_slice(data);
1599 }
1600 None => {
1601 buf.extend_from_slice(&(-1i32).to_be_bytes());
1602 }
1603 }
1604 }
1605 buf
1606 }
1607
1608 #[test]
1609 fn pg_data_row_get_i32() {
1610 let data = make_data_row(&[Some(&42i32.to_be_bytes())]);
1611 let row = PgDataRow::new(&data).unwrap();
1612 assert_eq!(row.get_i32(0), Some(42));
1613 assert_eq!(row.column_count(), 1);
1614 }
1615
1616 #[test]
1617 fn pg_data_row_get_i64() {
1618 let data = make_data_row(&[Some(&12345i64.to_be_bytes())]);
1619 let row = PgDataRow::new(&data).unwrap();
1620 assert_eq!(row.get_i64(0), Some(12345));
1621 }
1622
1623 #[test]
1624 fn pg_data_row_get_str() {
1625 let data = make_data_row(&[Some(b"hello")]);
1626 let row = PgDataRow::new(&data).unwrap();
1627 assert_eq!(row.get_str(0), Some("hello"));
1628 }
1629
1630 #[test]
1631 fn pg_data_row_get_bytes() {
1632 let data = make_data_row(&[Some(&[0xDE, 0xAD, 0xBE, 0xEF])]);
1633 let row = PgDataRow::new(&data).unwrap();
1634 assert_eq!(row.get_bytes(0), Some(&[0xDE, 0xAD, 0xBE, 0xEF][..]));
1635 }
1636
1637 #[test]
1638 fn pg_data_row_get_bool() {
1639 let data = make_data_row(&[Some(&[1u8])]);
1640 let row = PgDataRow::new(&data).unwrap();
1641 assert_eq!(row.get_bool(0), Some(true));
1642
1643 let data = make_data_row(&[Some(&[0u8])]);
1644 let row = PgDataRow::new(&data).unwrap();
1645 assert_eq!(row.get_bool(0), Some(false));
1646 }
1647
1648 #[test]
1649 fn pg_data_row_get_f64() {
1650 let data = make_data_row(&[Some(&3.14f64.to_be_bytes())]);
1651 let row = PgDataRow::new(&data).unwrap();
1652 assert!((row.get_f64(0).unwrap() - 3.14).abs() < 1e-10);
1653 }
1654
1655 #[test]
1656 fn pg_data_row_null_column() {
1657 let data = make_data_row(&[None]);
1658 let row = PgDataRow::new(&data).unwrap();
1659 assert!(row.is_null(0));
1660 assert_eq!(row.get_i32(0), None);
1661 assert_eq!(row.get_str(0), None);
1662 }
1663
1664 #[test]
1665 fn pg_data_row_multiple_columns() {
1666 let data = make_data_row(&[
1667 Some(&42i32.to_be_bytes()),
1668 Some(b"alice"),
1669 Some(b"alice@example.com"),
1670 Some(&[1u8]),
1671 Some(&3.14f64.to_be_bytes()),
1672 ]);
1673 let row = PgDataRow::new(&data).unwrap();
1674 assert_eq!(row.column_count(), 5);
1675 assert_eq!(row.get_i32(0), Some(42));
1676 assert_eq!(row.get_str(1), Some("alice"));
1677 assert_eq!(row.get_str(2), Some("alice@example.com"));
1678 assert_eq!(row.get_bool(3), Some(true));
1679 assert!((row.get_f64(4).unwrap() - 3.14).abs() < 1e-10);
1680 }
1681
1682 #[test]
1683 fn pg_data_row_mixed_null() {
1684 let data = make_data_row(&[Some(&42i32.to_be_bytes()), None, Some(b"text")]);
1685 let row = PgDataRow::new(&data).unwrap();
1686 assert_eq!(row.get_i32(0), Some(42));
1687 assert!(row.is_null(1));
1688 assert_eq!(row.get_str(1), None);
1689 assert_eq!(row.get_str(2), Some("text"));
1690 }
1691
1692 #[test]
1693 fn pg_data_row_empty() {
1694 let data = make_data_row(&[]);
1695 let row = PgDataRow::new(&data).unwrap();
1696 assert_eq!(row.column_count(), 0);
1697 }
1698
1699 #[test]
1700 fn pg_data_row_too_short() {
1701 let data = vec![0u8]; assert!(PgDataRow::new(&data).is_err());
1703 }
1704
1705 #[test]
1706 fn pg_data_row_truncated() {
1707 let mut data = Vec::new();
1709 data.extend_from_slice(&2i16.to_be_bytes());
1710 data.extend_from_slice(&4i32.to_be_bytes());
1711 data.extend_from_slice(&42i32.to_be_bytes());
1712 assert!(PgDataRow::new(&data).is_err());
1714 }
1715
1716 #[test]
1717 fn pg_data_row_get_i16() {
1718 let data = make_data_row(&[Some(&7i16.to_be_bytes())]);
1719 let row = PgDataRow::new(&data).unwrap();
1720 assert_eq!(row.get_i16(0), Some(7));
1721 }
1722
1723 #[test]
1724 fn pg_data_row_get_f32() {
1725 let data = make_data_row(&[Some(&2.5f32.to_be_bytes())]);
1726 let row = PgDataRow::new(&data).unwrap();
1727 assert!((row.get_f32(0).unwrap() - 2.5).abs() < 1e-6);
1728 }
1729
1730 #[test]
1731 fn pg_data_row_get_raw_null() {
1732 let data = make_data_row(&[None]);
1733 let row = PgDataRow::new(&data).unwrap();
1734 assert_eq!(row.get_raw(0), None);
1735 }
1736
1737 #[test]
1738 fn pg_data_row_get_raw_data() {
1739 let data = make_data_row(&[Some(&[1, 2, 3])]);
1740 let row = PgDataRow::new(&data).unwrap();
1741 assert_eq!(row.get_raw(0), Some(&[1u8, 2, 3][..]));
1742 }
1743
1744 #[test]
1745 fn pg_data_row_stack_alloc_16_columns() {
1746 let cols: Vec<Option<&[u8]>> = (0..16).map(|_| Some(&[0u8][..])).collect();
1748 let data = make_data_row(&cols);
1749 let row = PgDataRow::new(&data).unwrap();
1750 assert_eq!(row.column_count(), 16);
1751 for i in 0..16 {
1753 assert_eq!(row.get_raw(i), Some(&[0u8][..]));
1754 }
1755 }
1756
1757 #[test]
1762 fn inline_sequential_decode_five_columns() {
1763 let data = make_data_row(&[
1764 Some(&42i32.to_be_bytes()),
1765 Some(b"alice"),
1766 Some(b"alice@example.com"),
1767 Some(&[1u8]),
1768 Some(&3.14f64.to_be_bytes()),
1769 ]);
1770
1771 let mut pos: usize = 2; let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1776 pos += 4;
1777 assert_eq!(len, 4);
1778 let id = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1779 pos += len as usize;
1780 assert_eq!(id, 42);
1781
1782 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1784 pos += 4;
1785 assert_eq!(len, 5);
1786 let name = std::str::from_utf8(&data[pos..pos + len as usize]).unwrap();
1787 pos += len as usize;
1788 assert_eq!(name, "alice");
1789
1790 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1792 pos += 4;
1793 let email = std::str::from_utf8(&data[pos..pos + len as usize]).unwrap();
1794 pos += len as usize;
1795 assert_eq!(email, "alice@example.com");
1796
1797 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1799 pos += 4;
1800 assert_eq!(len, 1);
1801 let active = data[pos] != 0;
1802 pos += len as usize;
1803 assert!(active);
1804
1805 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1807 pos += 4;
1808 assert_eq!(len, 8);
1809 let score = f64::from_be_bytes([
1810 data[pos],
1811 data[pos + 1],
1812 data[pos + 2],
1813 data[pos + 3],
1814 data[pos + 4],
1815 data[pos + 5],
1816 data[pos + 6],
1817 data[pos + 7],
1818 ]);
1819 pos += len as usize;
1820 assert!((score - 3.14).abs() < 1e-10);
1821 assert_eq!(pos, data.len());
1822 }
1823
1824 #[test]
1826 fn inline_sequential_decode_with_nulls() {
1827 let data = make_data_row(&[
1828 Some(&42i32.to_be_bytes()),
1829 None, Some(b"text"),
1831 ]);
1832
1833 let mut pos: usize = 2;
1834
1835 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1837 pos += 4;
1838 let id = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1839 pos += len as usize;
1840 assert_eq!(id, 42);
1841
1842 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1844 pos += 4;
1845 let name: Option<&str> = if len < 0 {
1846 None
1847 } else {
1848 let s = std::str::from_utf8(&data[pos..pos + len as usize]).unwrap();
1849 pos += len as usize;
1850 Some(s)
1851 };
1852 assert!(name.is_none());
1853
1854 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1856 pos += 4;
1857 let txt = std::str::from_utf8(&data[pos..pos + len as usize]).unwrap();
1858 pos += len as usize;
1859 assert_eq!(txt, "text");
1860 assert_eq!(pos, data.len());
1861 }
1862
1863 #[test]
1865 fn inline_sequential_decode_all_scalar_types() {
1866 let data = make_data_row(&[
1867 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()), ]);
1874
1875 let mut pos: usize = 2;
1876
1877 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1879 pos += 4;
1880 let v_bool = data[pos] != 0;
1881 pos += len as usize;
1882 assert!(v_bool);
1883
1884 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1886 pos += 4;
1887 let v_i16 = i16::from_be_bytes([data[pos], data[pos + 1]]);
1888 pos += len as usize;
1889 assert_eq!(v_i16, 7);
1890
1891 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1893 pos += 4;
1894 let v_i32 = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1895 pos += len as usize;
1896 assert_eq!(v_i32, 42);
1897
1898 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1900 pos += 4;
1901 let v_i64 = i64::from_be_bytes([
1902 data[pos],
1903 data[pos + 1],
1904 data[pos + 2],
1905 data[pos + 3],
1906 data[pos + 4],
1907 data[pos + 5],
1908 data[pos + 6],
1909 data[pos + 7],
1910 ]);
1911 pos += len as usize;
1912 assert_eq!(v_i64, 12345);
1913
1914 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1916 pos += 4;
1917 let v_f32 = f32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1918 pos += len as usize;
1919 assert!((v_f32 - 2.5).abs() < 1e-6);
1920
1921 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1923 pos += 4;
1924 let v_f64 = f64::from_be_bytes([
1925 data[pos],
1926 data[pos + 1],
1927 data[pos + 2],
1928 data[pos + 3],
1929 data[pos + 4],
1930 data[pos + 5],
1931 data[pos + 6],
1932 data[pos + 7],
1933 ]);
1934 pos += len as usize;
1935 assert!((v_f64 - 3.14).abs() < 1e-10);
1936 assert_eq!(pos, data.len());
1937 }
1938
1939 #[test]
1941 fn pg_data_row_new_is_public() {
1942 let data = make_data_row(&[Some(&42i32.to_be_bytes())]);
1943 let row = PgDataRow::new(&data).unwrap();
1945 assert_eq!(row.get_i32(0), Some(42));
1946 }
1947
1948 #[test]
1950 fn inline_decode_matches_pgdatarow() {
1951 let data = make_data_row(&[
1952 Some(&99i32.to_be_bytes()),
1953 Some(b"hello world"),
1954 None,
1955 Some(&[0u8]),
1956 Some(&1.23f64.to_be_bytes()),
1957 ]);
1958
1959 let row = PgDataRow::new(&data).unwrap();
1961 let dr_i32 = row.get_i32(0);
1962 let dr_str = row.get_str(1);
1963 let dr_null = row.get_str(2);
1964 let dr_bool = row.get_bool(3);
1965 let dr_f64 = row.get_f64(4);
1966
1967 let mut pos: usize = 2;
1969
1970 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1971 pos += 4;
1972 let in_i32 = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1973 pos += len as usize;
1974
1975 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1976 pos += 4;
1977 let in_str = std::str::from_utf8(&data[pos..pos + len as usize]).unwrap();
1978 pos += len as usize;
1979
1980 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1981 pos += 4;
1982 let in_null: Option<&str> = if len < 0 { None } else { unreachable!() };
1983
1984 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1985 pos += 4;
1986 let in_bool = data[pos] != 0;
1987 pos += len as usize;
1988
1989 let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1990 pos += 4;
1991 let in_f64 = f64::from_be_bytes([
1992 data[pos],
1993 data[pos + 1],
1994 data[pos + 2],
1995 data[pos + 3],
1996 data[pos + 4],
1997 data[pos + 5],
1998 data[pos + 6],
1999 data[pos + 7],
2000 ]);
2001 pos += len as usize;
2002
2003 assert_eq!(dr_i32, Some(in_i32));
2005 assert_eq!(dr_str, Some(in_str));
2006 assert_eq!(dr_null, in_null);
2007 assert_eq!(dr_bool, Some(in_bool));
2008 assert!((dr_f64.unwrap() - in_f64).abs() < 1e-15);
2009 assert_eq!(pos, data.len());
2010 }
2011
2012 #[test]
2017 fn pg_data_row_all_null_columns() {
2018 let data = make_data_row(&[None, None, None, None, None]);
2019 let row = PgDataRow::new(&data).unwrap();
2020 assert_eq!(row.column_count(), 5);
2021 for i in 0..5 {
2022 assert!(row.is_null(i), "column {i} should be null");
2023 assert_eq!(row.get_raw(i), None);
2024 assert_eq!(row.get_i32(i), None);
2025 assert_eq!(row.get_i64(i), None);
2026 assert_eq!(row.get_str(i), None);
2027 assert_eq!(row.get_bool(i), None);
2028 assert_eq!(row.get_f64(i), None);
2029 }
2030 }
2031
2032 #[test]
2033 fn pg_data_row_very_long_text() {
2034 let long_text = "x".repeat(2048);
2035 let data = make_data_row(&[Some(long_text.as_bytes())]);
2036 let row = PgDataRow::new(&data).unwrap();
2037 assert_eq!(row.get_str(0), Some(long_text.as_str()));
2038 }
2039
2040 #[test]
2041 fn pg_data_row_empty_text() {
2042 let data = make_data_row(&[Some(b"")]);
2043 let row = PgDataRow::new(&data).unwrap();
2044 assert!(!row.is_null(0));
2045 assert_eq!(row.get_str(0), Some(""));
2046 assert_eq!(row.get_bytes(0), Some(&[][..]));
2047 }
2048
2049 #[test]
2050 fn pg_data_row_20_columns_exceeds_inline() {
2051 let col_data: Vec<[u8; 4]> = (0..20).map(|i: i32| i.to_be_bytes()).collect();
2052 let cols: Vec<Option<&[u8]>> = col_data.iter().map(|b| Some(b.as_slice())).collect();
2053 let data = make_data_row(&cols);
2054 let row = PgDataRow::new(&data).unwrap();
2055 assert_eq!(row.column_count(), 20);
2056 for i in 0..20 {
2057 assert_eq!(row.get_i32(i), Some(i as i32));
2058 }
2059 }
2060
2061 #[test]
2062 fn pg_data_row_is_null_each_position() {
2063 let data = make_data_row(&[Some(&1i32.to_be_bytes()), None, Some(&3i32.to_be_bytes())]);
2065 let row = PgDataRow::new(&data).unwrap();
2066 assert!(!row.is_null(0));
2067 assert!(row.is_null(1));
2068 assert!(!row.is_null(2));
2069 }
2070
2071 #[test]
2072 fn pg_data_row_negative_column_count() {
2073 let data = (-1i16).to_be_bytes();
2074 assert!(PgDataRow::new(&data).is_err());
2075 }
2076
2077 #[test]
2078 fn pg_data_row_get_str_invalid_utf8() {
2079 let invalid_utf8 = &[0xFF, 0xFE, 0x80];
2080 let data = make_data_row(&[Some(invalid_utf8)]);
2081 let row = PgDataRow::new(&data).unwrap();
2082 assert_eq!(row.get_str(0), None);
2084 assert_eq!(row.get_bytes(0), Some(&[0xFF, 0xFE, 0x80][..]));
2085 }
2086
2087 #[test]
2088 fn pg_data_row_get_i32_wrong_length() {
2089 let data = make_data_row(&[Some(&7i16.to_be_bytes())]);
2091 let row = PgDataRow::new(&data).unwrap();
2092 assert_eq!(row.get_i32(0), None); assert_eq!(row.get_i16(0), Some(7)); }
2095
2096 #[test]
2097 fn pg_data_row_get_i64_wrong_length() {
2098 let data = make_data_row(&[Some(&42i32.to_be_bytes())]);
2100 let row = PgDataRow::new(&data).unwrap();
2101 assert_eq!(row.get_i64(0), None);
2102 }
2103
2104 #[test]
2105 fn pg_data_row_get_f64_wrong_length() {
2106 let data = make_data_row(&[Some(&2.5f32.to_be_bytes())]);
2107 let row = PgDataRow::new(&data).unwrap();
2108 assert_eq!(row.get_f64(0), None); }
2110
2111 #[test]
2112 fn pg_data_row_get_f32_wrong_length() {
2113 let data = make_data_row(&[Some(&3.14f64.to_be_bytes())]);
2114 let row = PgDataRow::new(&data).unwrap();
2115 assert_eq!(row.get_f32(0), None); }
2117
2118 #[test]
2119 fn pg_data_row_get_bool_wrong_length() {
2120 let data = make_data_row(&[Some(&42i32.to_be_bytes())]);
2122 let row = PgDataRow::new(&data).unwrap();
2123 assert_eq!(row.get_bool(0), None);
2124 }
2125
2126 #[test]
2127 fn pg_data_row_unicode_text() {
2128 let texts = [
2129 "\u{1F600}\u{1F4A9}\u{1F680}", "\u{4e16}\u{754c}", "\u{0645}\u{0631}\u{062D}", "\u{1F468}\u{200D}\u{1F469}", ];
2134 for text in &texts {
2135 let data = make_data_row(&[Some(text.as_bytes())]);
2136 let row = PgDataRow::new(&data).unwrap();
2137 assert_eq!(row.get_str(0), Some(*text));
2138 }
2139 }
2140
2141 #[test]
2142 fn pg_data_row_i32_boundary_values() {
2143 for &val in &[i32::MIN, -1, 0, 1, i32::MAX] {
2144 let data = make_data_row(&[Some(&val.to_be_bytes())]);
2145 let row = PgDataRow::new(&data).unwrap();
2146 assert_eq!(row.get_i32(0), Some(val), "failed for {val}");
2147 }
2148 }
2149
2150 #[test]
2151 fn pg_data_row_i64_boundary_values() {
2152 for &val in &[i64::MIN, -1, 0, 1, i64::MAX] {
2153 let data = make_data_row(&[Some(&val.to_be_bytes())]);
2154 let row = PgDataRow::new(&data).unwrap();
2155 assert_eq!(row.get_i64(0), Some(val), "failed for {val}");
2156 }
2157 }
2158
2159 #[test]
2160 fn pg_data_row_f64_special_values() {
2161 let data = make_data_row(&[Some(&f64::INFINITY.to_be_bytes())]);
2162 let row = PgDataRow::new(&data).unwrap();
2163 assert_eq!(row.get_f64(0), Some(f64::INFINITY));
2164
2165 let data = make_data_row(&[Some(&f64::NEG_INFINITY.to_be_bytes())]);
2166 let row = PgDataRow::new(&data).unwrap();
2167 assert_eq!(row.get_f64(0), Some(f64::NEG_INFINITY));
2168
2169 let data = make_data_row(&[Some(&f64::NAN.to_be_bytes())]);
2170 let row = PgDataRow::new(&data).unwrap();
2171 assert!(row.get_f64(0).unwrap().is_nan());
2172 }
2173
2174 #[test]
2175 fn pg_data_row_f32_special_values() {
2176 let data = make_data_row(&[Some(&f32::INFINITY.to_be_bytes())]);
2177 let row = PgDataRow::new(&data).unwrap();
2178 assert_eq!(row.get_f32(0), Some(f32::INFINITY));
2179
2180 let data = make_data_row(&[Some(&f32::NAN.to_be_bytes())]);
2181 let row = PgDataRow::new(&data).unwrap();
2182 assert!(row.get_f32(0).unwrap().is_nan());
2183 }
2184
2185 #[test]
2186 fn pg_data_row_i16_boundary_values() {
2187 for &val in &[i16::MIN, -1, 0, 1, i16::MAX] {
2188 let data = make_data_row(&[Some(&val.to_be_bytes())]);
2189 let row = PgDataRow::new(&data).unwrap();
2190 assert_eq!(row.get_i16(0), Some(val));
2191 }
2192 }
2193
2194 mod proptest_fuzz {
2195 use super::*;
2196 use proptest::prelude::*;
2197
2198 proptest! {
2199 #[test]
2200 fn config_from_url_never_panics(url in ".*") {
2201 let _ = Config::from_url(&url);
2202 }
2203
2204 #[test]
2205 fn url_decode_never_panics(s in ".*") {
2206 let _ = url_decode(&s);
2207 }
2208
2209 #[test]
2210 fn pg_data_row_new_never_panics(data in proptest::collection::vec(any::<u8>(), 0..8192)) {
2211 let _ = PgDataRow::new(&data);
2212 }
2213
2214 #[test]
2215 fn hash_sql_never_panics(sql in ".*") {
2216 let _ = hash_sql(&sql);
2217 }
2218 }
2219 }
2220}