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 async fn live_type_oid(pool: &PgPool, typname: &str) -> u32 {
1107 let rows = pool
1108 .query_typed(
1109 "SELECT (oid::int8) AS result FROM pg_type WHERE typname = $1",
1110 &[DecodedValue::Str(typname.to_string())],
1111 &ExtensionOids::default(),
1112 )
1113 .await
1114 .unwrap();
1115 match rows.first() {
1116 Some(DecodedValue::I64(oid)) => *oid as u32,
1117 other => panic!("expected one int8 oid for {typname}, got {other:?}"),
1118 }
1119 }
1120
1121 #[tokio::test]
1124 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1125 async fn a_type_whose_oid_moved_under_a_live_pool_still_decodes() {
1126 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1127 pool.batch_execute(
1128 "DROP TYPE IF EXISTS pgcon_moved_enum; \
1129 CREATE TYPE pgcon_moved_enum AS ENUM ('a', 'b')",
1130 )
1131 .await
1132 .unwrap();
1133 pool.refresh_types().await.unwrap();
1135 let stale = pool.types();
1136 let before = live_type_oid(&pool, "pgcon_moved_enum").await;
1137 assert!(
1138 stale.enums.contains(&before),
1139 "the pool must start out knowing the pre-move OID, or this test proves nothing"
1140 );
1141
1142 pool.batch_execute(
1143 "DROP TYPE pgcon_moved_enum; \
1144 CREATE TYPE pgcon_moved_enum AS ENUM ('a', 'b')",
1145 )
1146 .await
1147 .unwrap();
1148 let after = live_type_oid(&pool, "pgcon_moved_enum").await;
1149 assert_ne!(before, after, "recreating a type must assign it a fresh OID");
1150
1151 let err = decode_value(after, b"a", &stale).unwrap_err();
1153 assert!(
1154 matches!(err, Error::UnknownTypeOid { oid } if oid == after),
1155 "expected UnknownTypeOid for the moved OID, got {err}"
1156 );
1157
1158 let rows = pool
1161 .query_composite("SELECT ('a'::pgcon_moved_enum) AS result", &pool.types())
1162 .await
1163 .unwrap();
1164 assert_eq!(rows, vec![DecodedValue::Str("a".to_string())]);
1165 assert!(
1166 pool.types().enums.contains(&after),
1167 "the heal must leave the new OID in the pool's registry, not just decode one query"
1168 );
1169 }
1170
1171 #[tokio::test]
1174 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1175 async fn a_refresh_through_one_handle_is_visible_through_every_clone() {
1176 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1177 pool.batch_execute("DROP TYPE IF EXISTS pgcon_clone_enum")
1178 .await
1179 .unwrap();
1180 pool.refresh_types().await.unwrap();
1181
1182 let clone = pool.clone();
1183 pool.batch_execute("CREATE TYPE pgcon_clone_enum AS ENUM ('a')")
1184 .await
1185 .unwrap();
1186 let oid = live_type_oid(&pool, "pgcon_clone_enum").await;
1187 assert!(!clone.types().enums.contains(&oid), "the clone must start out stale");
1188
1189 clone.refresh_types().await.unwrap();
1190
1191 assert!(pool.types().enums.contains(&oid), "the original must see the refresh");
1192 let tx = pool.begin_default().await.unwrap();
1193 assert!(
1194 tx.types().enums.contains(&oid),
1195 "a transaction opened after the refresh must inherit the refreshed registry"
1196 );
1197 tx.rollback().await.unwrap();
1198 pool.batch_execute("DROP TYPE pgcon_clone_enum").await.unwrap();
1199 }
1200
1201 async fn round_trip(pool: &PgPool, pg_type: &str, param: DecodedValue) -> DecodedValue {
1209 let sql = format!("SELECT ($1::{pg_type}) AS result");
1210 let rows = pool
1211 .query_typed(&sql, &[param], &ExtensionOids::default())
1212 .await
1213 .unwrap();
1214 rows.into_iter().next().unwrap()
1215 }
1216
1217 #[tokio::test]
1220 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1221 async fn query_explain_returns_parseable_json_with_a_plan_node() {
1222 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1223 let raw = pool.query_explain("SELECT 1 + 1", &[]).await.unwrap();
1224 let parsed: serde_json::Value = serde_json::from_str(&raw).unwrap();
1225 assert!(parsed[0]["Plan"]["Node Type"].is_string());
1226 assert!(parsed[0]["Plan"]["Actual Total Time"].is_number());
1228 }
1229
1230 #[tokio::test]
1231 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1232 async fn query_explain_binds_params_the_same_way_query_typed_does() {
1233 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1234 let raw = pool
1235 .query_explain("SELECT $1::int8 + 1", &[DecodedValue::I64(41)])
1236 .await
1237 .unwrap();
1238 let parsed: serde_json::Value = serde_json::from_str(&raw).unwrap();
1239 assert!(parsed[0]["Plan"]["Node Type"].is_string());
1240 }
1241
1242 #[tokio::test]
1243 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1244 async fn round_trips_bool_param() {
1245 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1246 assert_eq!(
1247 round_trip(&pool, "bool", DecodedValue::Bool(true)).await,
1248 DecodedValue::Bool(true)
1249 );
1250 assert_eq!(
1251 round_trip(&pool, "bool", DecodedValue::Bool(false)).await,
1252 DecodedValue::Bool(false)
1253 );
1254 }
1255
1256 #[tokio::test]
1257 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1258 async fn round_trips_integer_params_at_every_width() {
1259 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1260 assert_eq!(
1261 round_trip(&pool, "int2", DecodedValue::I64(30)).await,
1262 DecodedValue::I64(30)
1263 );
1264 assert_eq!(
1265 round_trip(&pool, "int4", DecodedValue::I64(70_000)).await,
1266 DecodedValue::I64(70_000)
1267 );
1268 assert_eq!(
1269 round_trip(&pool, "int8", DecodedValue::I64(9_223_372_036_854_775_807)).await,
1270 DecodedValue::I64(9_223_372_036_854_775_807)
1271 );
1272 assert_eq!(
1273 round_trip(&pool, "int8", DecodedValue::I64(-1)).await,
1274 DecodedValue::I64(-1)
1275 );
1276 }
1277
1278 #[tokio::test]
1279 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1280 async fn round_trips_float_params() {
1281 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1282 assert_eq!(
1283 round_trip(&pool, "float4", DecodedValue::F64(1.5)).await,
1284 DecodedValue::F64(1.5)
1285 );
1286 assert_eq!(
1287 round_trip(&pool, "float8", DecodedValue::F64(2.25)).await,
1288 DecodedValue::F64(2.25)
1289 );
1290 }
1291
1292 #[tokio::test]
1293 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1294 async fn round_trips_text_param() {
1295 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1296 assert_eq!(
1297 round_trip(&pool, "text", DecodedValue::Str("héllo 🎉".to_string())).await,
1298 DecodedValue::Str("héllo 🎉".to_string())
1299 );
1300 }
1301
1302 #[tokio::test]
1303 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1304 async fn round_trips_bytea_param() {
1305 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1306 assert_eq!(
1307 round_trip(&pool, "bytea", DecodedValue::Bytes(vec![1, 2, 3, 255])).await,
1308 DecodedValue::Bytes(vec![1, 2, 3, 255])
1309 );
1310 }
1311
1312 #[tokio::test]
1313 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1314 async fn round_trips_uuid_param() {
1315 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1316 let bytes = [0x11u8; 16];
1317 assert_eq!(
1318 round_trip(&pool, "uuid", DecodedValue::Uuid(bytes)).await,
1319 DecodedValue::Uuid(bytes)
1320 );
1321 }
1322
1323 #[tokio::test]
1324 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1325 async fn binds_a_plain_string_as_a_uuid_param() {
1326 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1334 let as_string = DecodedValue::Str("11111111-1111-1111-1111-111111111111".to_string());
1335 assert_eq!(
1336 round_trip(&pool, "uuid", as_string).await,
1337 DecodedValue::Uuid([0x11; 16])
1338 );
1339 }
1340
1341 #[tokio::test]
1342 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1343 async fn binds_a_plain_string_as_a_jsonb_param() {
1344 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1350 let as_string = DecodedValue::Str(r#"{"a":1,"b":[1,2]}"#.to_string());
1351 assert_eq!(
1352 round_trip(&pool, "jsonb", as_string).await,
1353 DecodedValue::Object(vec![
1354 ("a".into(), DecodedValue::I64(1)),
1355 (
1356 "b".into(),
1357 DecodedValue::Array(vec![DecodedValue::I64(1), DecodedValue::I64(2)])
1358 ),
1359 ])
1360 );
1361 }
1362
1363 #[tokio::test]
1364 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1365 async fn round_trips_numeric_param() {
1366 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1367 assert_eq!(
1368 round_trip(&pool, "numeric", DecodedValue::Decimal("12.50".to_string())).await,
1369 DecodedValue::Decimal("12.50".to_string())
1370 );
1371 assert_eq!(
1372 round_trip(&pool, "numeric", DecodedValue::Decimal("-9999.001".to_string())).await,
1373 DecodedValue::Decimal("-9999.001".to_string())
1374 );
1375
1376 for text in [
1381 "0.00000039999999999999998189924473035450347424557548947632312774658203125",
1382 "0.00000012222222222222222222222222222222222222222",
1383 "12222222222222222222222222222222000000000",
1384 "100000000000000000000000000000",
1385 "123456789012345678901234567890.123456789012345678901234567890",
1386 ] {
1387 assert_eq!(
1388 round_trip(&pool, "numeric", DecodedValue::Decimal(text.to_string())).await,
1389 DecodedValue::Decimal(text.to_string()),
1390 "{text}"
1391 );
1392 }
1393
1394 assert_eq!(
1397 round_trip(&pool, "numeric", DecodedValue::Str("0.0000004".to_string())).await,
1398 DecodedValue::Decimal("0.0000004".to_string())
1399 );
1400 assert_eq!(
1401 round_trip(&pool, "numeric", DecodedValue::I64(-12345)).await,
1402 DecodedValue::Decimal("-12345".to_string())
1403 );
1404 assert_eq!(
1405 round_trip(&pool, "numeric", DecodedValue::F64(0.1)).await,
1406 DecodedValue::Decimal("0.1".to_string())
1407 );
1408 }
1409
1410 #[tokio::test]
1411 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1412 async fn round_trips_null_param() {
1413 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1414 assert_eq!(round_trip(&pool, "int8", DecodedValue::Null).await, DecodedValue::Null);
1415 assert_eq!(round_trip(&pool, "text", DecodedValue::Null).await, DecodedValue::Null);
1416 }
1417
1418 #[tokio::test]
1419 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1420 async fn round_trips_array_param() {
1421 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1422 let param = DecodedValue::Array(vec![
1423 DecodedValue::Str("a".into()),
1424 DecodedValue::Str("b".into()),
1425 DecodedValue::Null,
1426 ]);
1427 assert_eq!(round_trip(&pool, "text[]", param.clone()).await, param);
1428 }
1429
1430 #[tokio::test]
1431 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1432 async fn round_trips_int_array_param() {
1433 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1434 let param = DecodedValue::Array(vec![DecodedValue::I64(1), DecodedValue::I64(2), DecodedValue::I64(3)]);
1435 assert_eq!(round_trip(&pool, "int8[]", param.clone()).await, param);
1436 }
1437
1438 #[tokio::test]
1439 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1440 async fn round_trips_jsonb_object_param() {
1441 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1442 let param = DecodedValue::Object(vec![
1443 ("a".into(), DecodedValue::I64(1)),
1444 ("b".into(), DecodedValue::Str("two".into())),
1445 (
1446 "c".into(),
1447 DecodedValue::Array(vec![DecodedValue::I64(1), DecodedValue::I64(2)]),
1448 ),
1449 ]);
1450 assert_eq!(round_trip(&pool, "jsonb", param.clone()).await, param);
1451 }
1452
1453 #[tokio::test]
1454 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1455 async fn query_typed_matches_pylon_cores_own_param_binding_convention() {
1456 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1459 let sql = "SELECT ($1::text, $2::int8, $3::bool) AS result";
1460 let params = vec![
1461 DecodedValue::Str("Alice".into()),
1462 DecodedValue::I64(30),
1463 DecodedValue::Bool(true),
1464 ];
1465 let rows = pool.query_typed(sql, ¶ms, &ExtensionOids::default()).await.unwrap();
1466 assert_eq!(
1467 rows,
1468 vec![DecodedValue::Composite(vec![
1469 DecodedValue::Str("Alice".into()),
1470 DecodedValue::I64(30),
1471 DecodedValue::Bool(true),
1472 ])]
1473 );
1474 }
1475
1476 #[tokio::test]
1477 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1478 async fn wrong_param_count_returns_an_error_not_a_panic() {
1479 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1480 let result = pool
1481 .query_typed(
1482 "SELECT $1::int8, $2::int8",
1483 &[DecodedValue::I64(1)],
1484 &ExtensionOids::default(),
1485 )
1486 .await;
1487 assert!(result.is_err());
1488 }
1489
1490 #[tokio::test]
1491 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1492 async fn execute_typed_runs_a_mutation_and_reports_affected_rows() {
1493 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1494 pool.query_raw("CREATE TEMP TABLE IF NOT EXISTS pgcon_execute_test (id int8, name text)")
1495 .await
1496 .unwrap();
1497
1498 let inserted = pool
1499 .execute_typed(
1500 "INSERT INTO pgcon_execute_test (id, name) VALUES ($1::int8, $2::text)",
1501 &[DecodedValue::I64(1), DecodedValue::Str("alice".into())],
1502 )
1503 .await
1504 .unwrap();
1505 assert_eq!(inserted, 1);
1506
1507 let updated = pool
1508 .execute_typed(
1509 "UPDATE pgcon_execute_test SET name = $1::text WHERE id = $2::int8",
1510 &[DecodedValue::Str("bob".into()), DecodedValue::I64(1)],
1511 )
1512 .await
1513 .unwrap();
1514 assert_eq!(updated, 1);
1515
1516 let rows = pool
1517 .query_composite(
1518 "SELECT (name) AS result FROM pgcon_execute_test",
1519 &ExtensionOids::default(),
1520 )
1521 .await
1522 .unwrap();
1523 assert_eq!(rows, vec![DecodedValue::Str("bob".to_string())]);
1524 }
1525
1526 #[tokio::test]
1534 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1535 async fn unique_violation_reports_23505() {
1536 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1537 pool.query_raw("CREATE TEMP TABLE pgcon_unique_test (id int8 PRIMARY KEY)")
1538 .await
1539 .unwrap();
1540 pool.execute_typed(
1541 "INSERT INTO pgcon_unique_test (id) VALUES ($1::int8)",
1542 &[DecodedValue::I64(1)],
1543 )
1544 .await
1545 .unwrap();
1546
1547 let err = pool
1548 .execute_typed(
1549 "INSERT INTO pgcon_unique_test (id) VALUES ($1::int8)",
1550 &[DecodedValue::I64(1)],
1551 )
1552 .await
1553 .unwrap_err();
1554 assert_eq!(err.sqlstate(), Some(&tokio_postgres::error::SqlState::UNIQUE_VIOLATION));
1555 assert_eq!(err.sqlstate().unwrap().code(), "23505");
1556 }
1557
1558 #[tokio::test]
1559 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1560 async fn foreign_key_violation_reports_23503() {
1561 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1562 pool.query_raw("CREATE TEMP TABLE pgcon_fk_parent (id int8 PRIMARY KEY)")
1563 .await
1564 .unwrap();
1565 pool.query_raw("CREATE TEMP TABLE pgcon_fk_child (parent_id int8 REFERENCES pgcon_fk_parent(id))")
1566 .await
1567 .unwrap();
1568
1569 let err = pool
1570 .execute_typed(
1571 "INSERT INTO pgcon_fk_child (parent_id) VALUES ($1::int8)",
1572 &[DecodedValue::I64(999)],
1573 )
1574 .await
1575 .unwrap_err();
1576 assert_eq!(
1577 err.sqlstate(),
1578 Some(&tokio_postgres::error::SqlState::FOREIGN_KEY_VIOLATION)
1579 );
1580 }
1581
1582 #[tokio::test]
1583 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1584 async fn check_violation_reports_23514() {
1585 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1586 pool.query_raw("CREATE TEMP TABLE pgcon_check_test (age int8 CHECK (age >= 0))")
1587 .await
1588 .unwrap();
1589
1590 let err = pool
1591 .execute_typed(
1592 "INSERT INTO pgcon_check_test (age) VALUES ($1::int8)",
1593 &[DecodedValue::I64(-1)],
1594 )
1595 .await
1596 .unwrap_err();
1597 assert_eq!(err.sqlstate(), Some(&tokio_postgres::error::SqlState::CHECK_VIOLATION));
1598 assert_eq!(err.violated_table().map(|(_, table)| table), Some("pgcon_check_test"));
1601 assert_eq!(err.violated_scalar(), None);
1602 }
1603
1604 #[tokio::test]
1605 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1606 async fn domain_check_violation_reports_the_domain_name_not_the_constraint_name() {
1607 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1608 pool.query_raw(
1609 "DO $$ BEGIN CREATE DOMAIN pgcon_rating AS int8 CHECK (VALUE BETWEEN 1 AND 5); \
1610 EXCEPTION WHEN duplicate_object THEN NULL; END $$",
1611 )
1612 .await
1613 .unwrap();
1614 pool.query_raw("CREATE TEMP TABLE pgcon_domain_check_test (rating pgcon_rating)")
1615 .await
1616 .unwrap();
1617
1618 let err = pool
1619 .execute_typed(
1620 "INSERT INTO pgcon_domain_check_test (rating) VALUES ($1::pgcon_rating)",
1621 &[DecodedValue::I64(99)],
1622 )
1623 .await
1624 .unwrap_err();
1625 assert_eq!(err.sqlstate(), Some(&tokio_postgres::error::SqlState::CHECK_VIOLATION));
1626 assert_eq!(err.violated_scalar(), Some(("public", "pgcon_rating")));
1627 }
1628
1629 #[tokio::test]
1630 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1631 async fn syntax_error_has_no_sqlstate_matching_constraint_codes() {
1632 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1633 let err = pool.query_raw("SELECT this is not valid sql").await.unwrap_err();
1634 assert_ne!(err.sqlstate(), Some(&tokio_postgres::error::SqlState::UNIQUE_VIOLATION));
1635 }
1636
1637 #[tokio::test]
1638 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1639 async fn connection_pool_error_has_no_sqlstate() {
1640 let result = PgPool::connect("not-a-valid-dsn", 5).await;
1643 let err = result.unwrap_err();
1644 assert_eq!(err.sqlstate(), None);
1645 }
1646
1647 #[tokio::test]
1650 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1651 async fn committed_transaction_persists_its_writes() {
1652 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1653 pool.query_raw("CREATE TEMP TABLE pgcon_tx_commit_test (id int8 PRIMARY KEY)")
1654 .await
1655 .unwrap();
1656
1657 let tx = pool.begin("serializable").await.unwrap();
1658 tx.execute_typed(
1659 "INSERT INTO pgcon_tx_commit_test (id) VALUES ($1::int8)",
1660 &[DecodedValue::I64(1)],
1661 )
1662 .await
1663 .unwrap();
1664 tx.commit().await.unwrap();
1665
1666 let rows = pool
1667 .query_composite(
1668 "SELECT (id) AS result FROM pgcon_tx_commit_test",
1669 &ExtensionOids::default(),
1670 )
1671 .await
1672 .unwrap();
1673 assert_eq!(rows, vec![DecodedValue::I64(1)]);
1674 }
1675
1676 #[tokio::test]
1677 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1678 async fn rolled_back_transaction_discards_its_writes() {
1679 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1680 pool.query_raw("CREATE TEMP TABLE pgcon_tx_rollback_test (id int8 PRIMARY KEY)")
1681 .await
1682 .unwrap();
1683
1684 let tx = pool.begin("serializable").await.unwrap();
1685 tx.execute_typed(
1686 "INSERT INTO pgcon_tx_rollback_test (id) VALUES ($1::int8)",
1687 &[DecodedValue::I64(1)],
1688 )
1689 .await
1690 .unwrap();
1691 tx.rollback().await.unwrap();
1692
1693 let rows = pool
1694 .query_composite(
1695 "SELECT (id) AS result FROM pgcon_tx_rollback_test",
1696 &ExtensionOids::default(),
1697 )
1698 .await
1699 .unwrap();
1700 assert!(rows.is_empty());
1701 }
1702
1703 #[tokio::test]
1704 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1705 async fn begin_actually_sets_the_requested_isolation_level() {
1706 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1707 for (level, expected) in [
1708 ("read_committed", "read committed"),
1709 ("repeatable_read", "repeatable read"),
1710 ("serializable", "serializable"),
1711 ] {
1712 let tx = pool.begin(level).await.unwrap();
1713 let rows = tx
1714 .query_typed(
1715 "SELECT (current_setting('transaction_isolation')) AS result",
1716 &[],
1717 &ExtensionOids::default(),
1718 )
1719 .await
1720 .unwrap();
1721 assert_eq!(rows, vec![DecodedValue::Str(expected.to_string())]);
1722 tx.rollback().await.unwrap();
1723 }
1724 }
1725
1726 #[tokio::test]
1727 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1728 async fn begin_rejects_an_unknown_isolation_level() {
1729 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1730 let result = pool.begin("not_a_real_level").await;
1731 assert!(result.is_err());
1732 }
1733
1734 #[tokio::test]
1735 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1736 async fn a_pooled_connection_is_reusable_after_commit_and_after_rollback() {
1737 let pool = PgPool::connect(&test_dsn(), 1).await.unwrap();
1744
1745 let tx = pool.begin("serializable").await.unwrap();
1746 tx.commit().await.unwrap();
1747
1748 let tx = pool.begin("serializable").await.unwrap();
1749 tx.rollback().await.unwrap();
1750
1751 let rows = pool.query_raw("SELECT 1").await.unwrap();
1752 assert_eq!(rows.len(), 1);
1753 }
1754
1755 #[tokio::test]
1756 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1757 async fn failed_commit_leaves_the_connection_reusable() {
1758 let pool = PgPool::connect(&test_dsn(), 2).await.unwrap();
1768 pool.query_raw("DROP TABLE IF EXISTS pgcon_tx_failed_commit_test")
1769 .await
1770 .unwrap();
1771 pool.query_raw("CREATE TABLE pgcon_tx_failed_commit_test (class int8, value int8)")
1772 .await
1773 .unwrap();
1774 pool.execute_typed(
1775 "INSERT INTO pgcon_tx_failed_commit_test (class, value) VALUES ($1::int8, $2::int8), ($3::int8, $4::int8)",
1776 &[
1777 DecodedValue::I64(1),
1778 DecodedValue::I64(10),
1779 DecodedValue::I64(2),
1780 DecodedValue::I64(20),
1781 ],
1782 )
1783 .await
1784 .unwrap();
1785
1786 let tx1 = pool.begin("serializable").await.unwrap();
1787 let tx2 = pool.begin("serializable").await.unwrap();
1788
1789 tx1.query_typed(
1790 "SELECT (sum(value)) AS result FROM pgcon_tx_failed_commit_test WHERE class = 1::int8",
1791 &[],
1792 &ExtensionOids::default(),
1793 )
1794 .await
1795 .unwrap();
1796 tx2.query_typed(
1797 "SELECT (sum(value)) AS result FROM pgcon_tx_failed_commit_test WHERE class = 2::int8",
1798 &[],
1799 &ExtensionOids::default(),
1800 )
1801 .await
1802 .unwrap();
1803 tx1.execute_typed(
1804 "INSERT INTO pgcon_tx_failed_commit_test (class, value) VALUES (2::int8, $1::int8)",
1805 &[DecodedValue::I64(10)],
1806 )
1807 .await
1808 .unwrap();
1809 tx2.execute_typed(
1810 "INSERT INTO pgcon_tx_failed_commit_test (class, value) VALUES (1::int8, $1::int8)",
1811 &[DecodedValue::I64(20)],
1812 )
1813 .await
1814 .unwrap();
1815
1816 tx1.commit().await.unwrap();
1817 let commit_result = tx2.commit().await;
1818 assert!(commit_result.is_err());
1819
1820 for _ in 0..2 {
1823 let rows = pool.query_raw("SELECT 1").await.unwrap();
1824 assert_eq!(rows.len(), 1);
1825 }
1826 }
1827
1828 #[tokio::test]
1829 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1830 async fn serializable_transactions_conflict_with_40001() {
1831 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1839 pool.query_raw("DROP TABLE IF EXISTS pgcon_serialization_test")
1840 .await
1841 .unwrap();
1842 pool.query_raw("CREATE TABLE pgcon_serialization_test (class int8, value int8)")
1843 .await
1844 .unwrap();
1845 pool.execute_typed(
1846 "INSERT INTO pgcon_serialization_test (class, value) VALUES ($1::int8, $2::int8), ($3::int8, $4::int8)",
1847 &[
1848 DecodedValue::I64(1),
1849 DecodedValue::I64(10),
1850 DecodedValue::I64(2),
1851 DecodedValue::I64(20),
1852 ],
1853 )
1854 .await
1855 .unwrap();
1856
1857 let tx1 = pool.begin("serializable").await.unwrap();
1858 let tx2 = pool.begin("serializable").await.unwrap();
1859
1860 tx1.query_typed(
1861 "SELECT (sum(value)) AS result FROM pgcon_serialization_test WHERE class = 1::int8",
1862 &[],
1863 &ExtensionOids::default(),
1864 )
1865 .await
1866 .unwrap();
1867 tx2.query_typed(
1868 "SELECT (sum(value)) AS result FROM pgcon_serialization_test WHERE class = 2::int8",
1869 &[],
1870 &ExtensionOids::default(),
1871 )
1872 .await
1873 .unwrap();
1874
1875 tx1.execute_typed(
1876 "INSERT INTO pgcon_serialization_test (class, value) VALUES (2::int8, $1::int8)",
1877 &[DecodedValue::I64(10)],
1878 )
1879 .await
1880 .unwrap();
1881 tx2.execute_typed(
1882 "INSERT INTO pgcon_serialization_test (class, value) VALUES (1::int8, $1::int8)",
1883 &[DecodedValue::I64(20)],
1884 )
1885 .await
1886 .unwrap();
1887
1888 tx1.commit().await.unwrap();
1889 let err = tx2.commit().await.unwrap_err();
1890 assert_eq!(
1891 err.sqlstate(),
1892 Some(&tokio_postgres::error::SqlState::T_R_SERIALIZATION_FAILURE)
1893 );
1894 }
1895
1896 #[tokio::test]
1897 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1898 async fn concurrent_transactions_deadlock_with_40p01() {
1899 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1905 pool.query_raw("DROP TABLE IF EXISTS pgcon_deadlock_test")
1906 .await
1907 .unwrap();
1908 pool.query_raw("CREATE TABLE pgcon_deadlock_test (id int8 PRIMARY KEY, value int8)")
1909 .await
1910 .unwrap();
1911 pool.execute_typed(
1912 "INSERT INTO pgcon_deadlock_test (id, value) VALUES ($1::int8, $2::int8), ($3::int8, $4::int8)",
1913 &[
1914 DecodedValue::I64(1),
1915 DecodedValue::I64(0),
1916 DecodedValue::I64(2),
1917 DecodedValue::I64(0),
1918 ],
1919 )
1920 .await
1921 .unwrap();
1922
1923 let tx1 = pool.begin("read_committed").await.unwrap();
1924 let tx2 = pool.begin("read_committed").await.unwrap();
1925
1926 tx1.execute_typed("UPDATE pgcon_deadlock_test SET value = 1::int8 WHERE id = 1::int8", &[])
1927 .await
1928 .unwrap();
1929 tx2.execute_typed("UPDATE pgcon_deadlock_test SET value = 2::int8 WHERE id = 2::int8", &[])
1930 .await
1931 .unwrap();
1932
1933 let (r1, r2) = tokio::join!(
1936 tx1.execute_typed("UPDATE pgcon_deadlock_test SET value = 3::int8 WHERE id = 2::int8", &[]),
1937 tx2.execute_typed("UPDATE pgcon_deadlock_test SET value = 4::int8 WHERE id = 1::int8", &[]),
1938 );
1939
1940 let results = [r1, r2];
1941 let deadlock_errors: Vec<_> = results
1942 .iter()
1943 .filter(|r| matches!(r, Err(e) if e.sqlstate() == Some(&tokio_postgres::error::SqlState::T_R_DEADLOCK_DETECTED)))
1944 .collect();
1945 assert_eq!(
1946 deadlock_errors.len(),
1947 1,
1948 "expected exactly one side to be aborted with 40P01, got {results:?}"
1949 );
1950 }
1951}
1952
1953#[cfg(test)]
1954mod error_message_tests {
1955 use super::*;
1956
1957 #[tokio::test]
1962 #[ignore = "requires a live database"]
1963 async fn a_server_error_renders_the_server_message() {
1964 let pool = PgPool::connect(&std::env::var("PYLON_PGCON_TEST_DSN").unwrap(), 2)
1965 .await
1966 .unwrap();
1967 let err = pool
1971 .query_typed("SELECT 1 FROM pylon_no_such_table", &[], &pool.types())
1972 .await
1973 .unwrap_err();
1974 let rendered = err.to_string();
1975 assert!(
1976 rendered.contains("pylon_no_such_table"),
1977 "expected the server's own message, got {rendered:?}"
1978 );
1979 assert_ne!(rendered, "db error");
1980 }
1981}