1pub mod error;
30pub mod listener;
31pub mod numeric;
32pub mod wire;
33
34pub use error::{Error, Result};
35pub use listener::PgListener;
36pub use wire::{ExtensionOids, decode_value};
37
38use pylon_value::DecodedValue;
39use std::sync::{Arc, RwLock};
40
41struct RawBytes<'a>(&'a [u8]);
48
49impl<'a> postgres_types::FromSql<'a> for RawBytes<'a> {
50 fn from_sql(
51 _ty: &postgres_types::Type,
52 raw: &'a [u8],
53 ) -> std::result::Result<Self, Box<dyn std::error::Error + Sync + Send>> {
54 Ok(RawBytes(raw))
55 }
56
57 fn accepts(_ty: &postgres_types::Type) -> bool {
58 true
59 }
60}
61
62#[derive(Debug)]
67struct BoundParam<'a>(&'a DecodedValue);
68
69impl postgres_types::ToSql for BoundParam<'_> {
70 fn to_sql(
71 &self,
72 ty: &postgres_types::Type,
73 out: &mut bytes::BytesMut,
74 ) -> std::result::Result<postgres_types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
75 Ok(wire::encode_value(self.0, ty, out)?)
76 }
77
78 fn accepts(_ty: &postgres_types::Type) -> bool {
79 true
80 }
81
82 postgres_types::to_sql_checked!();
83}
84
85#[derive(Clone, Debug)]
91pub struct PgPool {
92 pool: deadpool_postgres::Pool,
93 types: Arc<RwLock<Arc<ExtensionOids>>>,
97 name: String,
100}
101
102pub(crate) fn is_unknown_oid(err: &Error) -> bool {
106 matches!(err, Error::UnknownTypeOid { .. })
107}
108
109pub type PoolWaitObserver = fn(&str, std::time::Duration);
118
119static POOL_WAIT_OBSERVER: std::sync::OnceLock<PoolWaitObserver> = std::sync::OnceLock::new();
120
121pub fn set_pool_wait_observer(observer: PoolWaitObserver) {
124 let _ = POOL_WAIT_OBSERVER.set(observer);
125}
126
127pub struct PoolStatus {
132 pub size: usize,
134 pub available: usize,
136 pub waiting: usize,
138 pub max_size: usize,
139}
140
141impl PgPool {
142 pub async fn connect(dsn: &str, max_size: usize) -> Result<Self> {
158 let pg_config = session_config(dsn)?;
159 let manager = deadpool_postgres::Manager::new(pg_config, tokio_postgres::NoTls);
160 let pool = deadpool_postgres::Pool::builder(manager)
161 .max_size(max_size)
162 .create_timeout(Some(std::time::Duration::from_secs(10)))
163 .runtime(deadpool_postgres::Runtime::Tokio1)
164 .build()?;
165 let client = pool.get().await?;
166 let types = discover_types(&client).await?;
170 drop(client);
171 Ok(Self {
172 pool,
173 types: Arc::new(RwLock::new(Arc::new(types))),
174 name: "default".to_string(),
175 })
176 }
177
178 pub fn types(&self) -> Arc<ExtensionOids> {
184 self.types.read().unwrap().clone()
185 }
186
187 pub async fn refresh_types(&self) -> Result<()> {
196 let client = self.checkout().await?;
197 let fresh = discover_types(&client).await?;
198 *self.types.write().unwrap() = Arc::new(fresh);
199 drop(client);
200 self.clear_statement_caches();
201 Ok(())
202 }
203
204 async fn heal_types(&self, client: &tokio_postgres::Client) -> Result<Arc<ExtensionOids>> {
209 let fresh = Arc::new(discover_types(client).await?);
210 let changed = {
211 let mut slot = self.types.write().unwrap();
212 let changed = *slot != fresh;
213 if changed {
214 *slot = fresh.clone();
215 }
216 changed
217 };
218 if changed {
219 self.clear_statement_caches();
220 }
221 Ok(fresh)
222 }
223
224 pub fn clear_statement_caches(&self) {
231 self.pool.manager().statement_caches.clear();
232 }
233
234 pub fn set_name(&mut self, name: impl Into<String>) {
236 self.name = name.into();
237 }
238
239 async fn checkout(&self) -> Result<deadpool_postgres::Object> {
246 let started = std::time::Instant::now();
247 let client = self.pool.get().await?;
248 if let Some(observe) = POOL_WAIT_OBSERVER.get() {
249 observe(&self.name, started.elapsed());
250 }
251 Ok(client)
252 }
253
254 pub fn status(&self) -> PoolStatus {
257 let s = self.pool.status();
258 PoolStatus {
259 size: s.size,
260 available: s.available,
261 waiting: s.waiting,
262 max_size: s.max_size,
263 }
264 }
265
266 pub async fn query_raw(&self, sql: &str) -> Result<Vec<tokio_postgres::Row>> {
270 let client = self.checkout().await?;
271 let rows = client.query(sql, &[]).await?;
272 Ok(rows)
273 }
274
275 pub async fn query_composite(&self, sql: &str, ext: &ExtensionOids) -> Result<Vec<DecodedValue>> {
281 let client = self.checkout().await?;
282 let rows = client.query(sql, &[]).await?;
283 let decode = |ext: &ExtensionOids| -> Result<Vec<DecodedValue>> {
284 rows.iter().map(|r| decode_result_column(r, ext)).collect()
285 };
286 match decode(ext) {
287 Err(e) if is_unknown_oid(&e) => {
288 let healed = self.heal_types(&client).await?;
289 decode(&healed)
290 }
291 other => other,
292 }
293 }
294
295 pub async fn query_typed(
303 &self,
304 sql: &str,
305 params: &[DecodedValue],
306 ext: &ExtensionOids,
307 ) -> Result<Vec<DecodedValue>> {
308 let client = self.checkout().await?;
309 match query_typed_on(&client, sql, params, ext).await {
310 Err(e) if is_unknown_oid(&e) => {
311 let healed = self.heal_types(&client).await?;
312 query_typed_on(&client, sql, params, &healed).await
313 }
314 other => other,
315 }
316 }
317
318 pub async fn query_typed_named(
323 &self,
324 sql: &str,
325 params: &[DecodedValue],
326 ext: &ExtensionOids,
327 ) -> Result<Vec<DecodedValue>> {
328 let client = self.checkout().await?;
329 match query_typed_named_on(&client, sql, params, ext).await {
330 Err(e) if is_unknown_oid(&e) => {
331 let healed = self.heal_types(&client).await?;
332 query_typed_named_on(&client, sql, params, &healed).await
333 }
334 other => other,
335 }
336 }
337
338 pub async fn query_typed_with_globals(
343 &self,
344 sql: &str,
345 params: &[DecodedValue],
346 ext: &ExtensionOids,
347 globals: &str,
348 ) -> Result<Vec<DecodedValue>> {
349 let client = self.checkout().await?;
350 set_globals_on(&client, globals, false).await?;
351 match query_typed_on(&client, sql, params, ext).await {
352 Err(e) if is_unknown_oid(&e) => {
353 let healed = self.heal_types(&client).await?;
354 query_typed_on(&client, sql, params, &healed).await
355 }
356 other => other,
357 }
358 }
359
360 pub async fn execute_typed_with_globals(&self, sql: &str, params: &[DecodedValue], globals: &str) -> Result<u64> {
362 let client = self.checkout().await?;
363 set_globals_on(&client, globals, false).await?;
364 execute_typed_on(&client, sql, params).await
365 }
366
367 pub async fn execute_typed(&self, sql: &str, params: &[DecodedValue]) -> Result<u64> {
372 let client = self.checkout().await?;
373 execute_typed_on(&client, sql, params).await
374 }
375
376 pub async fn query_explain(&self, sql: &str, params: &[DecodedValue]) -> Result<String> {
383 let client = self.checkout().await?;
384 query_explain_on(&client, sql, params).await
385 }
386
387 pub async fn begin(&self, isolation: &str) -> Result<PgTransaction> {
395 let client = self.checkout().await?;
396 let level = match isolation {
397 "read_uncommitted" => "READ UNCOMMITTED",
398 "read_committed" => "READ COMMITTED",
399 "repeatable_read" => "REPEATABLE READ",
400 "serializable" => "SERIALIZABLE",
401 other => return Err(Error::message(format!("unknown isolation level: {other:?}"))),
402 };
403 client.batch_execute(&format!("BEGIN ISOLATION LEVEL {level}")).await?;
404 Ok(PgTransaction {
405 client,
406 types: self.types(),
407 })
408 }
409
410 pub async fn begin_default(&self) -> Result<PgTransaction> {
416 let client = self.checkout().await?;
417 client.batch_execute("BEGIN").await?;
418 Ok(PgTransaction {
419 client,
420 types: self.types(),
421 })
422 }
423
424 pub async fn batch_execute(&self, sql: &str) -> Result<()> {
432 let client = self.checkout().await?;
433 client.batch_execute(sql).await?;
434 Ok(())
435 }
436
437 pub async fn connection(&self) -> Result<PgConnection> {
447 let client = self.checkout().await?;
448 Ok(PgConnection {
449 client,
450 types: self.types(),
451 })
452 }
453}
454
455#[derive(Debug)]
458pub struct PgConnection {
459 client: deadpool_postgres::Object,
460 types: Arc<ExtensionOids>,
461}
462
463impl PgConnection {
464 pub fn types(&self) -> &ExtensionOids {
467 &self.types
468 }
469
470 pub async fn query_typed(
471 &self,
472 sql: &str,
473 params: &[DecodedValue],
474 ext: &ExtensionOids,
475 ) -> Result<Vec<DecodedValue>> {
476 query_typed_on(&self.client, sql, params, ext).await
477 }
478
479 pub async fn execute_typed(&self, sql: &str, params: &[DecodedValue]) -> Result<u64> {
480 execute_typed_on(&self.client, sql, params).await
481 }
482
483 pub async fn batch_execute(&self, sql: &str) -> Result<()> {
484 self.client.batch_execute(sql).await?;
485 Ok(())
486 }
487}
488
489async fn query_with_stmt(
499 client: &tokio_postgres::Client,
500 stmt: &tokio_postgres::Statement,
501 params: &[DecodedValue],
502 ext: &ExtensionOids,
503) -> Result<Vec<DecodedValue>> {
504 let bound: Vec<BoundParam<'_>> = params.iter().map(BoundParam).collect();
505 let param_refs: Vec<&(dyn postgres_types::ToSql + Sync)> =
506 bound.iter().map(|p| p as &(dyn postgres_types::ToSql + Sync)).collect();
507 let rows = client.query(stmt, ¶m_refs).await?;
508 rows.iter().map(|row| decode_result_column(row, ext)).collect()
509}
510
511async fn query_named_with_stmt(
515 client: &tokio_postgres::Client,
516 stmt: &tokio_postgres::Statement,
517 params: &[DecodedValue],
518 ext: &ExtensionOids,
519) -> Result<Vec<DecodedValue>> {
520 let bound: Vec<BoundParam<'_>> = params.iter().map(BoundParam).collect();
521 let param_refs: Vec<&(dyn postgres_types::ToSql + Sync)> =
522 bound.iter().map(|p| p as &(dyn postgres_types::ToSql + Sync)).collect();
523 let rows = client.query(stmt, ¶m_refs).await?;
524 rows.iter().map(|row| decode_row_named(row, ext)).collect()
525}
526
527async fn execute_with_stmt(
528 client: &tokio_postgres::Client,
529 stmt: &tokio_postgres::Statement,
530 params: &[DecodedValue],
531) -> Result<u64> {
532 let bound: Vec<BoundParam<'_>> = params.iter().map(BoundParam).collect();
533 let param_refs: Vec<&(dyn postgres_types::ToSql + Sync)> =
534 bound.iter().map(|p| p as &(dyn postgres_types::ToSql + Sync)).collect();
535 Ok(client.execute(stmt, ¶m_refs).await?)
536}
537
538pub(crate) async fn query_typed_on(
549 client: &deadpool_postgres::Object,
550 sql: &str,
551 params: &[DecodedValue],
552 ext: &ExtensionOids,
553) -> Result<Vec<DecodedValue>> {
554 let stmt = client.prepare_cached(sql).await?;
555 query_with_stmt(client, &stmt, params, ext).await
556}
557
558pub(crate) async fn query_typed_named_on(
560 client: &deadpool_postgres::Object,
561 sql: &str,
562 params: &[DecodedValue],
563 ext: &ExtensionOids,
564) -> Result<Vec<DecodedValue>> {
565 let stmt = client.prepare_cached(sql).await?;
566 query_named_with_stmt(client, &stmt, params, ext).await
567}
568
569async fn set_globals_on(client: &deadpool_postgres::Object, globals: &str, local: bool) -> Result<()> {
572 let sql = if local {
573 "SELECT set_config('pylon.globals', $1, true)"
574 } else {
575 "SELECT set_config('pylon.globals', $1, false)"
576 };
577 execute_typed_on(client, sql, &[DecodedValue::Str(globals.to_string())]).await?;
578 Ok(())
579}
580
581pub(crate) async fn execute_typed_on(
583 client: &deadpool_postgres::Object,
584 sql: &str,
585 params: &[DecodedValue],
586) -> Result<u64> {
587 let stmt = client.prepare_cached(sql).await?;
588 execute_with_stmt(client, &stmt, params).await
589}
590
591pub(crate) async fn query_typed_on_raw(
596 client: &tokio_postgres::Client,
597 sql: &str,
598 params: &[DecodedValue],
599 ext: &ExtensionOids,
600) -> Result<Vec<DecodedValue>> {
601 let stmt = client.prepare(sql).await?;
602 query_with_stmt(client, &stmt, params, ext).await
603}
604
605pub(crate) async fn query_typed_named_on_raw(
606 client: &tokio_postgres::Client,
607 sql: &str,
608 params: &[DecodedValue],
609 ext: &ExtensionOids,
610) -> Result<Vec<DecodedValue>> {
611 let stmt = client.prepare(sql).await?;
612 query_named_with_stmt(client, &stmt, params, ext).await
613}
614
615pub(crate) async fn execute_typed_on_raw(
616 client: &tokio_postgres::Client,
617 sql: &str,
618 params: &[DecodedValue],
619) -> Result<u64> {
620 let stmt = client.prepare(sql).await?;
621 execute_with_stmt(client, &stmt, params).await
622}
623
624pub(crate) async fn query_explain_on(
630 client: &deadpool_postgres::Object,
631 sql: &str,
632 params: &[DecodedValue],
633) -> Result<String> {
634 let wrapped = format!("EXPLAIN (ANALYZE, FORMAT JSON, VERBOSE) {sql}");
635 let stmt = client.prepare_cached(&wrapped).await?;
636 let bound: Vec<BoundParam<'_>> = params.iter().map(BoundParam).collect();
637 let param_refs: Vec<&(dyn postgres_types::ToSql + Sync)> =
638 bound.iter().map(|p| p as &(dyn postgres_types::ToSql + Sync)).collect();
639 let rows = client.query(&stmt, ¶m_refs).await?;
640 let row = rows
641 .into_iter()
642 .next()
643 .ok_or_else(|| Error::message("EXPLAIN produced no output row".to_string()))?;
644 let RawBytes(bytes) = row.try_get::<_, RawBytes<'_>>(0)?;
645 Ok(String::from_utf8_lossy(bytes).into_owned())
646}
647
648#[derive(Debug)]
660pub struct PgTransaction {
661 client: deadpool_postgres::Object,
662 types: Arc<ExtensionOids>,
663}
664
665impl PgTransaction {
666 pub fn types(&self) -> &ExtensionOids {
669 &self.types
670 }
671
672 pub async fn query_typed_with_globals(
675 &self,
676 sql: &str,
677 params: &[DecodedValue],
678 ext: &ExtensionOids,
679 globals: &str,
680 ) -> Result<Vec<DecodedValue>> {
681 set_globals_on(&self.client, globals, true).await?;
682 query_typed_on(&self.client, sql, params, ext).await
683 }
684
685 pub async fn execute_typed_with_globals(&self, sql: &str, params: &[DecodedValue], globals: &str) -> Result<u64> {
687 set_globals_on(&self.client, globals, true).await?;
688 execute_typed_on(&self.client, sql, params).await
689 }
690
691 pub async fn query_typed(
692 &self,
693 sql: &str,
694 params: &[DecodedValue],
695 ext: &ExtensionOids,
696 ) -> Result<Vec<DecodedValue>> {
697 query_typed_on(&self.client, sql, params, ext).await
698 }
699
700 pub async fn execute_typed(&self, sql: &str, params: &[DecodedValue]) -> Result<u64> {
701 execute_typed_on(&self.client, sql, params).await
702 }
703
704 pub async fn batch_execute(&self, sql: &str) -> Result<()> {
707 self.client.batch_execute(sql).await?;
708 Ok(())
709 }
710
711 pub async fn savepoint(&self, name: &str) -> Result<()> {
717 self.client
718 .batch_execute(&format!("SAVEPOINT {}", listener::quote_ident(name)))
719 .await?;
720 Ok(())
721 }
722
723 pub async fn release_savepoint(&self, name: &str) -> Result<()> {
724 self.client
725 .batch_execute(&format!("RELEASE SAVEPOINT {}", listener::quote_ident(name)))
726 .await?;
727 Ok(())
728 }
729
730 pub async fn rollback_to_savepoint(&self, name: &str) -> Result<()> {
731 self.client
732 .batch_execute(&format!("ROLLBACK TO SAVEPOINT {}", listener::quote_ident(name)))
733 .await?;
734 Ok(())
735 }
736
737 pub async fn commit(self) -> Result<()> {
742 match self.client.batch_execute("COMMIT").await {
743 Ok(()) => Ok(()),
744 Err(e) => {
745 let _ = self.client.batch_execute("ROLLBACK").await;
746 Err(e.into())
747 }
748 }
749 }
750
751 pub async fn rollback(self) -> Result<()> {
752 self.client.batch_execute("ROLLBACK").await?;
753 Ok(())
754 }
755}
756
757fn session_config(dsn: &str) -> Result<tokio_postgres::Config> {
776 let mut config: tokio_postgres::Config = dsn.parse()?;
777 let pinned = "-c timezone=UTC -c intervalstyle=iso_8601";
778 let options = match config.get_options() {
779 Some(existing) => format!("{existing} {pinned}"),
780 None => pinned.to_string(),
781 };
782 config.options(options);
783 Ok(config)
784}
785
786pub(crate) async fn discover_types(client: &tokio_postgres::Client) -> Result<ExtensionOids> {
790 let rows = client.query(wire::TYPE_DISCOVERY_SQL, &[]).await?;
791 Ok(ExtensionOids::from_discovery_rows(rows.iter().map(|r| {
792 let oid: i64 = r.get(0);
793 let typtype: String = r.get(1);
794 let base_oid: i64 = r.get(2);
795 let typname: String = r.get(3);
796 (oid as u32, typtype, base_oid as u32, typname)
797 })))
798}
799
800fn decode_result_column(row: &tokio_postgres::Row, ext: &ExtensionOids) -> Result<DecodedValue> {
801 let oid = row.columns()[0].type_().oid();
802 match row.try_get::<_, Option<RawBytes>>(0)? {
803 None => Ok(DecodedValue::Null),
804 Some(RawBytes(bytes)) => wire::decode_value(oid, bytes, ext),
805 }
806}
807
808fn decode_row_named(row: &tokio_postgres::Row, ext: &ExtensionOids) -> Result<DecodedValue> {
816 let mut fields = Vec::with_capacity(row.columns().len());
817 for (i, col) in row.columns().iter().enumerate() {
818 let oid = col.type_().oid();
819 let value = match row.try_get::<_, Option<RawBytes>>(i)? {
820 None => DecodedValue::Null,
821 Some(RawBytes(bytes)) => wire::decode_value(oid, bytes, ext)?,
822 };
823 fields.push((col.name().to_string(), value));
824 }
825 Ok(DecodedValue::Object(fields))
826}
827
828#[cfg(test)]
829mod tests {
830 use super::*;
831
832 fn test_dsn() -> String {
837 std::env::var("PYLON_PGCON_TEST_DSN").expect("PYLON_PGCON_TEST_DSN must be set to run live-Postgres tests")
838 }
839
840 #[tokio::test]
841 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
842 async fn connects_and_round_trips_a_scalar_query() {
843 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
844 let rows = pool.query_raw("SELECT 1 + 1").await.unwrap();
845 assert_eq!(rows.len(), 1);
846 let value: i32 = rows[0].get(0);
847 assert_eq!(value, 2);
848 }
849
850 #[tokio::test]
851 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
852 async fn pool_is_reused_across_multiple_queries() {
853 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
854 for i in 0..5 {
855 let rows = pool.query_raw(&format!("SELECT {i}")).await.unwrap();
856 let value: i32 = rows[0].get(0);
857 assert_eq!(value, i);
858 }
859 }
860
861 #[tokio::test]
862 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
863 async fn invalid_dsn_fails_to_connect() {
864 let result = PgPool::connect("not-a-valid-dsn", 5).await;
865 assert!(result.is_err());
866 }
867
868 #[tokio::test]
869 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
870 async fn connect_fails_eagerly_against_a_nonexistent_database() {
871 let dsn = test_dsn();
882 let (prefix, _db) = dsn.rsplit_once('/').expect("DSN must have a database path segment");
883 let bad_dsn = format!("{prefix}/pgcon_definitely_does_not_exist");
884 let result = PgPool::connect(&bad_dsn, 5).await;
885 assert!(result.is_err());
886 }
887
888 #[tokio::test]
889 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
890 async fn bad_sql_returns_an_error_not_a_panic() {
891 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
892 let result = pool.query_raw("SELECT this is not valid sql").await;
893 assert!(result.is_err());
894 }
895
896 #[tokio::test]
905 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
906 async fn decodes_a_bare_scalar_result_column() {
907 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
908 let rows = pool
909 .query_composite("SELECT 42::int8 AS result", &ExtensionOids::default())
910 .await
911 .unwrap();
912 assert_eq!(rows, vec![DecodedValue::I64(42)]);
913 }
914
915 #[tokio::test]
916 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
917 async fn decodes_a_composite_matching_pylon_cores_own_emission_shape() {
918 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
921 let sql = "SELECT ('Person'::text, 'Alice'::text, 30::int8, NULL::text) AS result";
922 let rows = pool.query_composite(sql, &ExtensionOids::default()).await.unwrap();
923 assert_eq!(
924 rows,
925 vec![DecodedValue::Composite(vec![
926 DecodedValue::Str("Person".into()),
927 DecodedValue::Str("Alice".into()),
928 DecodedValue::I64(30),
929 DecodedValue::Null,
930 ])]
931 );
932 }
933
934 #[tokio::test]
935 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
936 async fn decodes_nested_composite_and_array_of_composite_for_real() {
937 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
938 let sql = "SELECT (\
939 'Product'::text, \
940 ROW('Tag'::text, 'sale'::text), \
941 ARRAY[ROW(1::int8), ROW(2::int8)]::record[]\
942 ) AS result";
943 let rows = pool.query_composite(sql, &ExtensionOids::default()).await.unwrap();
944 assert_eq!(
945 rows,
946 vec![DecodedValue::Composite(vec![
947 DecodedValue::Str("Product".into()),
948 DecodedValue::Composite(vec![DecodedValue::Str("Tag".into()), DecodedValue::Str("sale".into())]),
949 DecodedValue::Array(vec![
950 DecodedValue::Composite(vec![DecodedValue::I64(1)]),
951 DecodedValue::Composite(vec![DecodedValue::I64(2)]),
952 ]),
953 ])]
954 );
955 }
956
957 #[tokio::test]
958 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
959 async fn decodes_array_of_text_for_real() {
960 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
961 let sql = "SELECT (ARRAY['a', 'b', NULL]::text[]) AS result";
962 let rows = pool.query_composite(sql, &ExtensionOids::default()).await.unwrap();
963 assert_eq!(
964 rows,
965 vec![DecodedValue::Array(vec![
966 DecodedValue::Str("a".into()),
967 DecodedValue::Str("b".into()),
968 DecodedValue::Null,
969 ])]
970 );
971 }
972
973 #[tokio::test]
974 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
975 async fn decodes_numeric_and_jsonb_and_uuid_for_real() {
976 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
977 let sql = "SELECT (\
978 12.50::numeric, \
979 '{\"a\": 1, \"b\": [1,2]}'::jsonb, \
980 '11111111-1111-1111-1111-111111111111'::uuid\
981 ) AS result";
982 let rows = pool.query_composite(sql, &ExtensionOids::default()).await.unwrap();
983 let DecodedValue::Composite(fields) = &rows[0] else {
984 panic!("expected Composite")
985 };
986 assert_eq!(fields[0], DecodedValue::Decimal("12.50".to_string()));
987 assert_eq!(
988 fields[1],
989 DecodedValue::Object(vec![
990 ("a".into(), DecodedValue::I64(1)),
991 (
992 "b".into(),
993 DecodedValue::Array(vec![DecodedValue::I64(1), DecodedValue::I64(2)])
994 ),
995 ])
996 );
997 assert_eq!(fields[2], DecodedValue::Uuid([0x11; 16]));
998 }
999
1000 #[tokio::test]
1001 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1002 async fn decodes_bytea_for_real() {
1003 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1004 let rows = pool
1005 .query_composite("SELECT '\\xdeadbeef'::bytea AS result", &ExtensionOids::default())
1006 .await
1007 .unwrap();
1008 assert_eq!(rows, vec![DecodedValue::Bytes(vec![0xde, 0xad, 0xbe, 0xef])]);
1009 }
1010
1011 #[tokio::test]
1012 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1013 async fn decodes_enum_cast_to_text_for_real() {
1014 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1018 let sql = "DO $$ BEGIN CREATE TYPE pgcon_test_enum AS ENUM ('a', 'b'); \
1019 EXCEPTION WHEN duplicate_object THEN NULL; END $$;";
1020 pool.query_raw(sql).await.ok();
1021 let rows = pool
1022 .query_composite(
1023 "SELECT ('a'::pgcon_test_enum::text) AS result",
1024 &ExtensionOids::default(),
1025 )
1026 .await
1027 .unwrap();
1028 assert_eq!(rows, vec![DecodedValue::Str("a".to_string())]);
1029 }
1030
1031 #[tokio::test]
1032 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1033 async fn decodes_a_real_pgvector_value_through_connect_time_discovery() {
1034 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1035 if pool.query_raw("CREATE EXTENSION IF NOT EXISTS vector").await.is_err() {
1036 eprintln!("skipping: pgvector not installable on this server");
1037 return;
1038 }
1039 pool.refresh_types().await.unwrap();
1041 assert!(
1042 pool.types().vector.is_some(),
1043 "discovery should have found the vector OID"
1044 );
1045
1046 let rows = pool
1048 .query_composite("SELECT ('doc', '[1.5,2.5]'::vector) AS result", &pool.types())
1049 .await
1050 .unwrap();
1051
1052 let DecodedValue::Composite(fields) = &rows[0] else {
1053 panic!("expected Composite, got {:?}", rows[0])
1054 };
1055 assert_eq!(fields[0], DecodedValue::Str("doc".to_string()));
1056 assert_eq!(
1057 fields[1],
1058 DecodedValue::Array(vec![DecodedValue::F64(1.5), DecodedValue::F64(2.5)]),
1059 "a vector must decode to its floats, not to mojibake or an error"
1060 );
1061 }
1062
1063 #[tokio::test]
1064 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1065 async fn decodes_a_real_enum_and_domain_through_connect_time_discovery() {
1066 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1067 pool.query_raw(
1068 "DO $$ BEGIN CREATE TYPE pgcon_disc_enum AS ENUM ('x', 'y'); \
1069 EXCEPTION WHEN duplicate_object THEN NULL; END $$;",
1070 )
1071 .await
1072 .ok();
1073 pool.query_raw(
1074 "DO $$ BEGIN CREATE DOMAIN pgcon_disc_domain AS int8 CHECK (VALUE > 0); \
1075 EXCEPTION WHEN duplicate_object THEN NULL; END $$;",
1076 )
1077 .await
1078 .ok();
1079
1080 pool.refresh_types().await.unwrap();
1081
1082 let rows = pool
1085 .query_composite(
1086 "SELECT ('x'::pgcon_disc_enum, 42::pgcon_disc_domain) AS result",
1087 &pool.types(),
1088 )
1089 .await
1090 .unwrap();
1091
1092 let DecodedValue::Composite(fields) = &rows[0] else {
1093 panic!("expected Composite, got {:?}", rows[0])
1094 };
1095 assert_eq!(fields[0], DecodedValue::Str("x".to_string()));
1096 assert_eq!(
1097 fields[1],
1098 DecodedValue::I64(42),
1099 "a domain must decode as its base type, not as text"
1100 );
1101 }
1102
1103 #[tokio::test]
1104 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1105 async fn decodes_a_real_named_composite_through_connect_time_discovery() {
1106 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1109 pool.query_raw(
1110 "DO $$ BEGIN CREATE TYPE pgcon_disc_tuple AS (\"amount\" numeric, \"note\" text); \
1111 EXCEPTION WHEN duplicate_object THEN NULL; END $$;",
1112 )
1113 .await
1114 .ok();
1115
1116 pool.refresh_types().await.unwrap();
1117
1118 let rows = pool
1119 .query_composite(
1120 "SELECT (
1121 ROW(12.3400, 'x')::pgcon_disc_tuple,
1122 ARRAY[ROW(1.500, 'y')::pgcon_disc_tuple]
1123 ) AS result",
1124 &pool.types(),
1125 )
1126 .await
1127 .unwrap();
1128
1129 let DecodedValue::Composite(fields) = &rows[0] else {
1130 panic!("expected Composite, got {:?}", rows[0])
1131 };
1132 assert_eq!(
1133 fields[0],
1134 DecodedValue::Composite(vec![
1135 DecodedValue::Decimal("12.3400".to_string()),
1136 DecodedValue::Str("x".to_string()),
1137 ]),
1138 "each member keeps its own type, scale included"
1139 );
1140 assert_eq!(
1141 fields[1],
1142 DecodedValue::Array(vec![DecodedValue::Composite(vec![
1143 DecodedValue::Decimal("1.500".to_string()),
1144 DecodedValue::Str("y".to_string()),
1145 ])]),
1146 "an array of them reads element-wise"
1147 );
1148 }
1149
1150 async fn live_type_oid(pool: &PgPool, typname: &str) -> u32 {
1154 let rows = pool
1155 .query_typed(
1156 "SELECT (oid::int8) AS result FROM pg_type WHERE typname = $1",
1157 &[DecodedValue::Str(typname.to_string())],
1158 &ExtensionOids::default(),
1159 )
1160 .await
1161 .unwrap();
1162 match rows.first() {
1163 Some(DecodedValue::I64(oid)) => *oid as u32,
1164 other => panic!("expected one int8 oid for {typname}, got {other:?}"),
1165 }
1166 }
1167
1168 #[tokio::test]
1171 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1172 async fn a_type_whose_oid_moved_under_a_live_pool_still_decodes() {
1173 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1174 pool.batch_execute(
1175 "DROP TYPE IF EXISTS pgcon_moved_enum; \
1176 CREATE TYPE pgcon_moved_enum AS ENUM ('a', 'b')",
1177 )
1178 .await
1179 .unwrap();
1180 pool.refresh_types().await.unwrap();
1182 let stale = pool.types();
1183 let before = live_type_oid(&pool, "pgcon_moved_enum").await;
1184 assert!(
1185 stale.enums.contains(&before),
1186 "the pool must start out knowing the pre-move OID, or this test proves nothing"
1187 );
1188
1189 pool.batch_execute(
1190 "DROP TYPE pgcon_moved_enum; \
1191 CREATE TYPE pgcon_moved_enum AS ENUM ('a', 'b')",
1192 )
1193 .await
1194 .unwrap();
1195 let after = live_type_oid(&pool, "pgcon_moved_enum").await;
1196 assert_ne!(before, after, "recreating a type must assign it a fresh OID");
1197
1198 let err = decode_value(after, b"a", &stale).unwrap_err();
1200 assert!(
1201 matches!(err, Error::UnknownTypeOid { oid } if oid == after),
1202 "expected UnknownTypeOid for the moved OID, got {err}"
1203 );
1204
1205 let rows = pool
1208 .query_composite("SELECT ('a'::pgcon_moved_enum) AS result", &pool.types())
1209 .await
1210 .unwrap();
1211 assert_eq!(rows, vec![DecodedValue::Str("a".to_string())]);
1212 assert!(
1213 pool.types().enums.contains(&after),
1214 "the heal must leave the new OID in the pool's registry, not just decode one query"
1215 );
1216 }
1217
1218 #[tokio::test]
1221 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1222 async fn a_refresh_through_one_handle_is_visible_through_every_clone() {
1223 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1224 pool.batch_execute("DROP TYPE IF EXISTS pgcon_clone_enum")
1225 .await
1226 .unwrap();
1227 pool.refresh_types().await.unwrap();
1228
1229 let clone = pool.clone();
1230 pool.batch_execute("CREATE TYPE pgcon_clone_enum AS ENUM ('a')")
1231 .await
1232 .unwrap();
1233 let oid = live_type_oid(&pool, "pgcon_clone_enum").await;
1234 assert!(!clone.types().enums.contains(&oid), "the clone must start out stale");
1235
1236 clone.refresh_types().await.unwrap();
1237
1238 assert!(pool.types().enums.contains(&oid), "the original must see the refresh");
1239 let tx = pool.begin_default().await.unwrap();
1240 assert!(
1241 tx.types().enums.contains(&oid),
1242 "a transaction opened after the refresh must inherit the refreshed registry"
1243 );
1244 tx.rollback().await.unwrap();
1245 pool.batch_execute("DROP TYPE pgcon_clone_enum").await.unwrap();
1246 }
1247
1248 async fn round_trip(pool: &PgPool, pg_type: &str, param: DecodedValue) -> DecodedValue {
1256 let sql = format!("SELECT ($1::{pg_type}) AS result");
1257 let rows = pool
1258 .query_typed(&sql, &[param], &ExtensionOids::default())
1259 .await
1260 .unwrap();
1261 rows.into_iter().next().unwrap()
1262 }
1263
1264 #[tokio::test]
1267 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1268 async fn query_explain_returns_parseable_json_with_a_plan_node() {
1269 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1270 let raw = pool.query_explain("SELECT 1 + 1", &[]).await.unwrap();
1271 let parsed: serde_json::Value = serde_json::from_str(&raw).unwrap();
1272 assert!(parsed[0]["Plan"]["Node Type"].is_string());
1273 assert!(parsed[0]["Plan"]["Actual Total Time"].is_number());
1275 }
1276
1277 #[tokio::test]
1278 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1279 async fn query_explain_binds_params_the_same_way_query_typed_does() {
1280 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1281 let raw = pool
1282 .query_explain("SELECT $1::int8 + 1", &[DecodedValue::I64(41)])
1283 .await
1284 .unwrap();
1285 let parsed: serde_json::Value = serde_json::from_str(&raw).unwrap();
1286 assert!(parsed[0]["Plan"]["Node Type"].is_string());
1287 }
1288
1289 #[tokio::test]
1290 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1291 async fn round_trips_bool_param() {
1292 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1293 assert_eq!(
1294 round_trip(&pool, "bool", DecodedValue::Bool(true)).await,
1295 DecodedValue::Bool(true)
1296 );
1297 assert_eq!(
1298 round_trip(&pool, "bool", DecodedValue::Bool(false)).await,
1299 DecodedValue::Bool(false)
1300 );
1301 }
1302
1303 #[tokio::test]
1304 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1305 async fn round_trips_integer_params_at_every_width() {
1306 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1307 assert_eq!(
1308 round_trip(&pool, "int2", DecodedValue::I64(30)).await,
1309 DecodedValue::I64(30)
1310 );
1311 assert_eq!(
1312 round_trip(&pool, "int4", DecodedValue::I64(70_000)).await,
1313 DecodedValue::I64(70_000)
1314 );
1315 assert_eq!(
1316 round_trip(&pool, "int8", DecodedValue::I64(9_223_372_036_854_775_807)).await,
1317 DecodedValue::I64(9_223_372_036_854_775_807)
1318 );
1319 assert_eq!(
1320 round_trip(&pool, "int8", DecodedValue::I64(-1)).await,
1321 DecodedValue::I64(-1)
1322 );
1323 }
1324
1325 #[tokio::test]
1326 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1327 async fn round_trips_float_params() {
1328 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1329 assert_eq!(
1330 round_trip(&pool, "float4", DecodedValue::F64(1.5)).await,
1331 DecodedValue::F64(1.5)
1332 );
1333 assert_eq!(
1334 round_trip(&pool, "float8", DecodedValue::F64(2.25)).await,
1335 DecodedValue::F64(2.25)
1336 );
1337 }
1338
1339 #[tokio::test]
1340 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1341 async fn round_trips_text_param() {
1342 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1343 assert_eq!(
1344 round_trip(&pool, "text", DecodedValue::Str("héllo 🎉".to_string())).await,
1345 DecodedValue::Str("héllo 🎉".to_string())
1346 );
1347 }
1348
1349 #[tokio::test]
1350 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1351 async fn round_trips_bytea_param() {
1352 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1353 assert_eq!(
1354 round_trip(&pool, "bytea", DecodedValue::Bytes(vec![1, 2, 3, 255])).await,
1355 DecodedValue::Bytes(vec![1, 2, 3, 255])
1356 );
1357 }
1358
1359 #[tokio::test]
1360 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1361 async fn round_trips_uuid_param() {
1362 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1363 let bytes = [0x11u8; 16];
1364 assert_eq!(
1365 round_trip(&pool, "uuid", DecodedValue::Uuid(bytes)).await,
1366 DecodedValue::Uuid(bytes)
1367 );
1368 }
1369
1370 #[tokio::test]
1371 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1372 async fn binds_a_plain_string_as_a_uuid_param() {
1373 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1381 let as_string = DecodedValue::Str("11111111-1111-1111-1111-111111111111".to_string());
1382 assert_eq!(
1383 round_trip(&pool, "uuid", as_string).await,
1384 DecodedValue::Uuid([0x11; 16])
1385 );
1386 }
1387
1388 #[tokio::test]
1389 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1390 async fn binds_a_plain_string_as_a_jsonb_param() {
1391 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1397 let as_string = DecodedValue::Str(r#"{"a":1,"b":[1,2]}"#.to_string());
1398 assert_eq!(
1399 round_trip(&pool, "jsonb", as_string).await,
1400 DecodedValue::Object(vec![
1401 ("a".into(), DecodedValue::I64(1)),
1402 (
1403 "b".into(),
1404 DecodedValue::Array(vec![DecodedValue::I64(1), DecodedValue::I64(2)])
1405 ),
1406 ])
1407 );
1408 }
1409
1410 #[tokio::test]
1411 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1412 async fn round_trips_numeric_param() {
1413 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1414 assert_eq!(
1415 round_trip(&pool, "numeric", DecodedValue::Decimal("12.50".to_string())).await,
1416 DecodedValue::Decimal("12.50".to_string())
1417 );
1418 assert_eq!(
1419 round_trip(&pool, "numeric", DecodedValue::Decimal("-9999.001".to_string())).await,
1420 DecodedValue::Decimal("-9999.001".to_string())
1421 );
1422
1423 for text in [
1428 "0.00000039999999999999998189924473035450347424557548947632312774658203125",
1429 "0.00000012222222222222222222222222222222222222222",
1430 "12222222222222222222222222222222000000000",
1431 "100000000000000000000000000000",
1432 "123456789012345678901234567890.123456789012345678901234567890",
1433 ] {
1434 assert_eq!(
1435 round_trip(&pool, "numeric", DecodedValue::Decimal(text.to_string())).await,
1436 DecodedValue::Decimal(text.to_string()),
1437 "{text}"
1438 );
1439 }
1440
1441 assert_eq!(
1444 round_trip(&pool, "numeric", DecodedValue::Str("0.0000004".to_string())).await,
1445 DecodedValue::Decimal("0.0000004".to_string())
1446 );
1447 assert_eq!(
1448 round_trip(&pool, "numeric", DecodedValue::I64(-12345)).await,
1449 DecodedValue::Decimal("-12345".to_string())
1450 );
1451 assert_eq!(
1452 round_trip(&pool, "numeric", DecodedValue::F64(0.1)).await,
1453 DecodedValue::Decimal("0.1".to_string())
1454 );
1455 }
1456
1457 #[tokio::test]
1458 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1459 async fn round_trips_null_param() {
1460 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1461 assert_eq!(round_trip(&pool, "int8", DecodedValue::Null).await, DecodedValue::Null);
1462 assert_eq!(round_trip(&pool, "text", DecodedValue::Null).await, DecodedValue::Null);
1463 }
1464
1465 #[tokio::test]
1466 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1467 async fn round_trips_array_param() {
1468 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1469 let param = DecodedValue::Array(vec![
1470 DecodedValue::Str("a".into()),
1471 DecodedValue::Str("b".into()),
1472 DecodedValue::Null,
1473 ]);
1474 assert_eq!(round_trip(&pool, "text[]", param.clone()).await, param);
1475 }
1476
1477 #[tokio::test]
1478 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1479 async fn round_trips_int_array_param() {
1480 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1481 let param = DecodedValue::Array(vec![DecodedValue::I64(1), DecodedValue::I64(2), DecodedValue::I64(3)]);
1482 assert_eq!(round_trip(&pool, "int8[]", param.clone()).await, param);
1483 }
1484
1485 #[tokio::test]
1486 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1487 async fn round_trips_jsonb_object_param() {
1488 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1489 let param = DecodedValue::Object(vec![
1490 ("a".into(), DecodedValue::I64(1)),
1491 ("b".into(), DecodedValue::Str("two".into())),
1492 (
1493 "c".into(),
1494 DecodedValue::Array(vec![DecodedValue::I64(1), DecodedValue::I64(2)]),
1495 ),
1496 ]);
1497 assert_eq!(round_trip(&pool, "jsonb", param.clone()).await, param);
1498 }
1499
1500 #[tokio::test]
1501 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1502 async fn query_typed_matches_pylon_cores_own_param_binding_convention() {
1503 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1506 let sql = "SELECT ($1::text, $2::int8, $3::bool) AS result";
1507 let params = vec![
1508 DecodedValue::Str("Alice".into()),
1509 DecodedValue::I64(30),
1510 DecodedValue::Bool(true),
1511 ];
1512 let rows = pool.query_typed(sql, ¶ms, &ExtensionOids::default()).await.unwrap();
1513 assert_eq!(
1514 rows,
1515 vec![DecodedValue::Composite(vec![
1516 DecodedValue::Str("Alice".into()),
1517 DecodedValue::I64(30),
1518 DecodedValue::Bool(true),
1519 ])]
1520 );
1521 }
1522
1523 #[tokio::test]
1524 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1525 async fn wrong_param_count_returns_an_error_not_a_panic() {
1526 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1527 let result = pool
1528 .query_typed(
1529 "SELECT $1::int8, $2::int8",
1530 &[DecodedValue::I64(1)],
1531 &ExtensionOids::default(),
1532 )
1533 .await;
1534 assert!(result.is_err());
1535 }
1536
1537 #[tokio::test]
1538 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1539 async fn execute_typed_runs_a_mutation_and_reports_affected_rows() {
1540 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1541 pool.query_raw("CREATE TEMP TABLE IF NOT EXISTS pgcon_execute_test (id int8, name text)")
1542 .await
1543 .unwrap();
1544
1545 let inserted = pool
1546 .execute_typed(
1547 "INSERT INTO pgcon_execute_test (id, name) VALUES ($1::int8, $2::text)",
1548 &[DecodedValue::I64(1), DecodedValue::Str("alice".into())],
1549 )
1550 .await
1551 .unwrap();
1552 assert_eq!(inserted, 1);
1553
1554 let updated = pool
1555 .execute_typed(
1556 "UPDATE pgcon_execute_test SET name = $1::text WHERE id = $2::int8",
1557 &[DecodedValue::Str("bob".into()), DecodedValue::I64(1)],
1558 )
1559 .await
1560 .unwrap();
1561 assert_eq!(updated, 1);
1562
1563 let rows = pool
1564 .query_composite(
1565 "SELECT (name) AS result FROM pgcon_execute_test",
1566 &ExtensionOids::default(),
1567 )
1568 .await
1569 .unwrap();
1570 assert_eq!(rows, vec![DecodedValue::Str("bob".to_string())]);
1571 }
1572
1573 #[tokio::test]
1581 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1582 async fn unique_violation_reports_23505() {
1583 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1584 pool.query_raw("CREATE TEMP TABLE pgcon_unique_test (id int8 PRIMARY KEY)")
1585 .await
1586 .unwrap();
1587 pool.execute_typed(
1588 "INSERT INTO pgcon_unique_test (id) VALUES ($1::int8)",
1589 &[DecodedValue::I64(1)],
1590 )
1591 .await
1592 .unwrap();
1593
1594 let err = pool
1595 .execute_typed(
1596 "INSERT INTO pgcon_unique_test (id) VALUES ($1::int8)",
1597 &[DecodedValue::I64(1)],
1598 )
1599 .await
1600 .unwrap_err();
1601 assert_eq!(err.sqlstate(), Some(&tokio_postgres::error::SqlState::UNIQUE_VIOLATION));
1602 assert_eq!(err.sqlstate().unwrap().code(), "23505");
1603 }
1604
1605 #[tokio::test]
1606 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1607 async fn foreign_key_violation_reports_23503() {
1608 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1609 pool.query_raw("CREATE TEMP TABLE pgcon_fk_parent (id int8 PRIMARY KEY)")
1610 .await
1611 .unwrap();
1612 pool.query_raw("CREATE TEMP TABLE pgcon_fk_child (parent_id int8 REFERENCES pgcon_fk_parent(id))")
1613 .await
1614 .unwrap();
1615
1616 let err = pool
1617 .execute_typed(
1618 "INSERT INTO pgcon_fk_child (parent_id) VALUES ($1::int8)",
1619 &[DecodedValue::I64(999)],
1620 )
1621 .await
1622 .unwrap_err();
1623 assert_eq!(
1624 err.sqlstate(),
1625 Some(&tokio_postgres::error::SqlState::FOREIGN_KEY_VIOLATION)
1626 );
1627 }
1628
1629 #[tokio::test]
1630 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1631 async fn check_violation_reports_23514() {
1632 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1633 pool.query_raw("CREATE TEMP TABLE pgcon_check_test (age int8 CHECK (age >= 0))")
1634 .await
1635 .unwrap();
1636
1637 let err = pool
1638 .execute_typed(
1639 "INSERT INTO pgcon_check_test (age) VALUES ($1::int8)",
1640 &[DecodedValue::I64(-1)],
1641 )
1642 .await
1643 .unwrap_err();
1644 assert_eq!(err.sqlstate(), Some(&tokio_postgres::error::SqlState::CHECK_VIOLATION));
1645 assert_eq!(err.violated_table().map(|(_, table)| table), Some("pgcon_check_test"));
1648 assert_eq!(err.violated_scalar(), None);
1649 }
1650
1651 #[tokio::test]
1652 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1653 async fn domain_check_violation_reports_the_domain_name_not_the_constraint_name() {
1654 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1655 pool.query_raw(
1656 "DO $$ BEGIN CREATE DOMAIN pgcon_rating AS int8 CHECK (VALUE BETWEEN 1 AND 5); \
1657 EXCEPTION WHEN duplicate_object THEN NULL; END $$",
1658 )
1659 .await
1660 .unwrap();
1661 pool.query_raw("CREATE TEMP TABLE pgcon_domain_check_test (rating pgcon_rating)")
1662 .await
1663 .unwrap();
1664
1665 let err = pool
1666 .execute_typed(
1667 "INSERT INTO pgcon_domain_check_test (rating) VALUES ($1::pgcon_rating)",
1668 &[DecodedValue::I64(99)],
1669 )
1670 .await
1671 .unwrap_err();
1672 assert_eq!(err.sqlstate(), Some(&tokio_postgres::error::SqlState::CHECK_VIOLATION));
1673 assert_eq!(err.violated_scalar(), Some(("public", "pgcon_rating")));
1674 }
1675
1676 #[tokio::test]
1677 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1678 async fn syntax_error_has_no_sqlstate_matching_constraint_codes() {
1679 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1680 let err = pool.query_raw("SELECT this is not valid sql").await.unwrap_err();
1681 assert_ne!(err.sqlstate(), Some(&tokio_postgres::error::SqlState::UNIQUE_VIOLATION));
1682 }
1683
1684 #[tokio::test]
1685 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1686 async fn connection_pool_error_has_no_sqlstate() {
1687 let result = PgPool::connect("not-a-valid-dsn", 5).await;
1690 let err = result.unwrap_err();
1691 assert_eq!(err.sqlstate(), None);
1692 }
1693
1694 #[tokio::test]
1697 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1698 async fn committed_transaction_persists_its_writes() {
1699 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1700 pool.query_raw("CREATE TEMP TABLE pgcon_tx_commit_test (id int8 PRIMARY KEY)")
1701 .await
1702 .unwrap();
1703
1704 let tx = pool.begin("serializable").await.unwrap();
1705 tx.execute_typed(
1706 "INSERT INTO pgcon_tx_commit_test (id) VALUES ($1::int8)",
1707 &[DecodedValue::I64(1)],
1708 )
1709 .await
1710 .unwrap();
1711 tx.commit().await.unwrap();
1712
1713 let rows = pool
1714 .query_composite(
1715 "SELECT (id) AS result FROM pgcon_tx_commit_test",
1716 &ExtensionOids::default(),
1717 )
1718 .await
1719 .unwrap();
1720 assert_eq!(rows, vec![DecodedValue::I64(1)]);
1721 }
1722
1723 #[tokio::test]
1724 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1725 async fn rolled_back_transaction_discards_its_writes() {
1726 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1727 pool.query_raw("CREATE TEMP TABLE pgcon_tx_rollback_test (id int8 PRIMARY KEY)")
1728 .await
1729 .unwrap();
1730
1731 let tx = pool.begin("serializable").await.unwrap();
1732 tx.execute_typed(
1733 "INSERT INTO pgcon_tx_rollback_test (id) VALUES ($1::int8)",
1734 &[DecodedValue::I64(1)],
1735 )
1736 .await
1737 .unwrap();
1738 tx.rollback().await.unwrap();
1739
1740 let rows = pool
1741 .query_composite(
1742 "SELECT (id) AS result FROM pgcon_tx_rollback_test",
1743 &ExtensionOids::default(),
1744 )
1745 .await
1746 .unwrap();
1747 assert!(rows.is_empty());
1748 }
1749
1750 #[tokio::test]
1751 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1752 async fn begin_actually_sets_the_requested_isolation_level() {
1753 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1754 for (level, expected) in [
1755 ("read_committed", "read committed"),
1756 ("repeatable_read", "repeatable read"),
1757 ("serializable", "serializable"),
1758 ] {
1759 let tx = pool.begin(level).await.unwrap();
1760 let rows = tx
1761 .query_typed(
1762 "SELECT (current_setting('transaction_isolation')) AS result",
1763 &[],
1764 &ExtensionOids::default(),
1765 )
1766 .await
1767 .unwrap();
1768 assert_eq!(rows, vec![DecodedValue::Str(expected.to_string())]);
1769 tx.rollback().await.unwrap();
1770 }
1771 }
1772
1773 #[tokio::test]
1774 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1775 async fn begin_rejects_an_unknown_isolation_level() {
1776 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1777 let result = pool.begin("not_a_real_level").await;
1778 assert!(result.is_err());
1779 }
1780
1781 #[tokio::test]
1782 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1783 async fn a_pooled_connection_is_reusable_after_commit_and_after_rollback() {
1784 let pool = PgPool::connect(&test_dsn(), 1).await.unwrap();
1791
1792 let tx = pool.begin("serializable").await.unwrap();
1793 tx.commit().await.unwrap();
1794
1795 let tx = pool.begin("serializable").await.unwrap();
1796 tx.rollback().await.unwrap();
1797
1798 let rows = pool.query_raw("SELECT 1").await.unwrap();
1799 assert_eq!(rows.len(), 1);
1800 }
1801
1802 #[tokio::test]
1803 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1804 async fn failed_commit_leaves_the_connection_reusable() {
1805 let pool = PgPool::connect(&test_dsn(), 2).await.unwrap();
1815 pool.query_raw("DROP TABLE IF EXISTS pgcon_tx_failed_commit_test")
1816 .await
1817 .unwrap();
1818 pool.query_raw("CREATE TABLE pgcon_tx_failed_commit_test (class int8, value int8)")
1819 .await
1820 .unwrap();
1821 pool.execute_typed(
1822 "INSERT INTO pgcon_tx_failed_commit_test (class, value) VALUES ($1::int8, $2::int8), ($3::int8, $4::int8)",
1823 &[
1824 DecodedValue::I64(1),
1825 DecodedValue::I64(10),
1826 DecodedValue::I64(2),
1827 DecodedValue::I64(20),
1828 ],
1829 )
1830 .await
1831 .unwrap();
1832
1833 let tx1 = pool.begin("serializable").await.unwrap();
1834 let tx2 = pool.begin("serializable").await.unwrap();
1835
1836 tx1.query_typed(
1837 "SELECT (sum(value)) AS result FROM pgcon_tx_failed_commit_test WHERE class = 1::int8",
1838 &[],
1839 &ExtensionOids::default(),
1840 )
1841 .await
1842 .unwrap();
1843 tx2.query_typed(
1844 "SELECT (sum(value)) AS result FROM pgcon_tx_failed_commit_test WHERE class = 2::int8",
1845 &[],
1846 &ExtensionOids::default(),
1847 )
1848 .await
1849 .unwrap();
1850 tx1.execute_typed(
1851 "INSERT INTO pgcon_tx_failed_commit_test (class, value) VALUES (2::int8, $1::int8)",
1852 &[DecodedValue::I64(10)],
1853 )
1854 .await
1855 .unwrap();
1856 tx2.execute_typed(
1857 "INSERT INTO pgcon_tx_failed_commit_test (class, value) VALUES (1::int8, $1::int8)",
1858 &[DecodedValue::I64(20)],
1859 )
1860 .await
1861 .unwrap();
1862
1863 tx1.commit().await.unwrap();
1864 let commit_result = tx2.commit().await;
1865 assert!(commit_result.is_err());
1866
1867 for _ in 0..2 {
1870 let rows = pool.query_raw("SELECT 1").await.unwrap();
1871 assert_eq!(rows.len(), 1);
1872 }
1873 }
1874
1875 #[tokio::test]
1876 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1877 async fn serializable_transactions_conflict_with_40001() {
1878 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1886 pool.query_raw("DROP TABLE IF EXISTS pgcon_serialization_test")
1887 .await
1888 .unwrap();
1889 pool.query_raw("CREATE TABLE pgcon_serialization_test (class int8, value int8)")
1890 .await
1891 .unwrap();
1892 pool.execute_typed(
1893 "INSERT INTO pgcon_serialization_test (class, value) VALUES ($1::int8, $2::int8), ($3::int8, $4::int8)",
1894 &[
1895 DecodedValue::I64(1),
1896 DecodedValue::I64(10),
1897 DecodedValue::I64(2),
1898 DecodedValue::I64(20),
1899 ],
1900 )
1901 .await
1902 .unwrap();
1903
1904 let tx1 = pool.begin("serializable").await.unwrap();
1905 let tx2 = pool.begin("serializable").await.unwrap();
1906
1907 tx1.query_typed(
1908 "SELECT (sum(value)) AS result FROM pgcon_serialization_test WHERE class = 1::int8",
1909 &[],
1910 &ExtensionOids::default(),
1911 )
1912 .await
1913 .unwrap();
1914 tx2.query_typed(
1915 "SELECT (sum(value)) AS result FROM pgcon_serialization_test WHERE class = 2::int8",
1916 &[],
1917 &ExtensionOids::default(),
1918 )
1919 .await
1920 .unwrap();
1921
1922 tx1.execute_typed(
1923 "INSERT INTO pgcon_serialization_test (class, value) VALUES (2::int8, $1::int8)",
1924 &[DecodedValue::I64(10)],
1925 )
1926 .await
1927 .unwrap();
1928 tx2.execute_typed(
1929 "INSERT INTO pgcon_serialization_test (class, value) VALUES (1::int8, $1::int8)",
1930 &[DecodedValue::I64(20)],
1931 )
1932 .await
1933 .unwrap();
1934
1935 tx1.commit().await.unwrap();
1936 let err = tx2.commit().await.unwrap_err();
1937 assert_eq!(
1938 err.sqlstate(),
1939 Some(&tokio_postgres::error::SqlState::T_R_SERIALIZATION_FAILURE)
1940 );
1941 }
1942
1943 #[tokio::test]
1944 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1945 async fn concurrent_transactions_deadlock_with_40p01() {
1946 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1952 pool.query_raw("DROP TABLE IF EXISTS pgcon_deadlock_test")
1953 .await
1954 .unwrap();
1955 pool.query_raw("CREATE TABLE pgcon_deadlock_test (id int8 PRIMARY KEY, value int8)")
1956 .await
1957 .unwrap();
1958 pool.execute_typed(
1959 "INSERT INTO pgcon_deadlock_test (id, value) VALUES ($1::int8, $2::int8), ($3::int8, $4::int8)",
1960 &[
1961 DecodedValue::I64(1),
1962 DecodedValue::I64(0),
1963 DecodedValue::I64(2),
1964 DecodedValue::I64(0),
1965 ],
1966 )
1967 .await
1968 .unwrap();
1969
1970 let tx1 = pool.begin("read_committed").await.unwrap();
1971 let tx2 = pool.begin("read_committed").await.unwrap();
1972
1973 tx1.execute_typed("UPDATE pgcon_deadlock_test SET value = 1::int8 WHERE id = 1::int8", &[])
1974 .await
1975 .unwrap();
1976 tx2.execute_typed("UPDATE pgcon_deadlock_test SET value = 2::int8 WHERE id = 2::int8", &[])
1977 .await
1978 .unwrap();
1979
1980 let (r1, r2) = tokio::join!(
1983 tx1.execute_typed("UPDATE pgcon_deadlock_test SET value = 3::int8 WHERE id = 2::int8", &[]),
1984 tx2.execute_typed("UPDATE pgcon_deadlock_test SET value = 4::int8 WHERE id = 1::int8", &[]),
1985 );
1986
1987 let results = [r1, r2];
1988 let deadlock_errors: Vec<_> = results
1989 .iter()
1990 .filter(|r| matches!(r, Err(e) if e.sqlstate() == Some(&tokio_postgres::error::SqlState::T_R_DEADLOCK_DETECTED)))
1991 .collect();
1992 assert_eq!(
1993 deadlock_errors.len(),
1994 1,
1995 "expected exactly one side to be aborted with 40P01, got {results:?}"
1996 );
1997 }
1998}
1999
2000#[cfg(test)]
2001mod error_message_tests {
2002 use super::*;
2003
2004 #[tokio::test]
2009 #[ignore = "requires a live database"]
2010 async fn a_server_error_renders_the_server_message() {
2011 let pool = PgPool::connect(&std::env::var("PYLON_PGCON_TEST_DSN").unwrap(), 2)
2012 .await
2013 .unwrap();
2014 let err = pool
2018 .query_typed("SELECT 1 FROM pylon_no_such_table", &[], &pool.types())
2019 .await
2020 .unwrap_err();
2021 let rendered = err.to_string();
2022 assert!(
2023 rendered.contains("pylon_no_such_table"),
2024 "expected the server's own message, got {rendered:?}"
2025 );
2026 assert_ne!(rendered, "db error");
2027 }
2028}