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;
39
40struct RawBytes<'a>(&'a [u8]);
47
48impl<'a> postgres_types::FromSql<'a> for RawBytes<'a> {
49 fn from_sql(
50 _ty: &postgres_types::Type,
51 raw: &'a [u8],
52 ) -> std::result::Result<Self, Box<dyn std::error::Error + Sync + Send>> {
53 Ok(RawBytes(raw))
54 }
55
56 fn accepts(_ty: &postgres_types::Type) -> bool {
57 true
58 }
59}
60
61#[derive(Debug)]
66struct BoundParam<'a>(&'a DecodedValue);
67
68impl postgres_types::ToSql for BoundParam<'_> {
69 fn to_sql(
70 &self,
71 ty: &postgres_types::Type,
72 out: &mut bytes::BytesMut,
73 ) -> std::result::Result<postgres_types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
74 Ok(wire::encode_value(self.0, ty, out)?)
75 }
76
77 fn accepts(_ty: &postgres_types::Type) -> bool {
78 true
79 }
80
81 postgres_types::to_sql_checked!();
82}
83
84#[derive(Clone, Debug)]
90pub struct PgPool {
91 pool: deadpool_postgres::Pool,
92 types: ExtensionOids,
93 name: String,
96}
97
98pub type PoolWaitObserver = fn(&str, std::time::Duration);
107
108static POOL_WAIT_OBSERVER: std::sync::OnceLock<PoolWaitObserver> = std::sync::OnceLock::new();
109
110pub fn set_pool_wait_observer(observer: PoolWaitObserver) {
113 let _ = POOL_WAIT_OBSERVER.set(observer);
114}
115
116pub struct PoolStatus {
121 pub size: usize,
123 pub available: usize,
125 pub waiting: usize,
127 pub max_size: usize,
128}
129
130impl PgPool {
131 pub async fn connect(dsn: &str, max_size: usize) -> Result<Self> {
147 let pg_config = session_config(dsn)?;
148 let manager = deadpool_postgres::Manager::new(pg_config, tokio_postgres::NoTls);
149 let pool = deadpool_postgres::Pool::builder(manager)
150 .max_size(max_size)
151 .create_timeout(Some(std::time::Duration::from_secs(10)))
152 .runtime(deadpool_postgres::Runtime::Tokio1)
153 .build()?;
154 let client = pool.get().await?;
155 let types = discover_types(&client).await?;
159 drop(client);
160 Ok(Self {
161 pool,
162 types,
163 name: "default".to_string(),
164 })
165 }
166
167 pub fn types(&self) -> &ExtensionOids {
172 &self.types
173 }
174
175 pub async fn refresh_types(&mut self) -> Result<()> {
179 let client = self.checkout().await?;
180 self.types = discover_types(&client).await?;
181 drop(client);
186 self.clear_statement_caches();
187 Ok(())
188 }
189
190 pub fn clear_statement_caches(&self) {
197 self.pool.manager().statement_caches.clear();
198 }
199
200 pub fn set_name(&mut self, name: impl Into<String>) {
202 self.name = name.into();
203 }
204
205 async fn checkout(&self) -> Result<deadpool_postgres::Object> {
212 let started = std::time::Instant::now();
213 let client = self.pool.get().await?;
214 if let Some(observe) = POOL_WAIT_OBSERVER.get() {
215 observe(&self.name, started.elapsed());
216 }
217 Ok(client)
218 }
219
220 pub fn status(&self) -> PoolStatus {
223 let s = self.pool.status();
224 PoolStatus {
225 size: s.size,
226 available: s.available,
227 waiting: s.waiting,
228 max_size: s.max_size,
229 }
230 }
231
232 pub async fn query_raw(&self, sql: &str) -> Result<Vec<tokio_postgres::Row>> {
236 let client = self.checkout().await?;
237 let rows = client.query(sql, &[]).await?;
238 Ok(rows)
239 }
240
241 pub async fn query_composite(&self, sql: &str, ext: &ExtensionOids) -> Result<Vec<DecodedValue>> {
247 let rows = self.query_raw(sql).await?;
248 rows.iter().map(|row| decode_result_column(row, ext)).collect()
249 }
250
251 pub async fn query_typed(
259 &self,
260 sql: &str,
261 params: &[DecodedValue],
262 ext: &ExtensionOids,
263 ) -> Result<Vec<DecodedValue>> {
264 let client = self.checkout().await?;
265 query_typed_on(&client, sql, params, ext).await
266 }
267
268 pub async fn query_typed_named(
273 &self,
274 sql: &str,
275 params: &[DecodedValue],
276 ext: &ExtensionOids,
277 ) -> Result<Vec<DecodedValue>> {
278 let client = self.checkout().await?;
279 query_typed_named_on(&client, sql, params, ext).await
280 }
281
282 pub async fn query_typed_with_globals(
287 &self,
288 sql: &str,
289 params: &[DecodedValue],
290 ext: &ExtensionOids,
291 globals: &str,
292 ) -> Result<Vec<DecodedValue>> {
293 let client = self.checkout().await?;
294 set_globals_on(&client, globals, false).await?;
295 query_typed_on(&client, sql, params, ext).await
296 }
297
298 pub async fn execute_typed_with_globals(&self, sql: &str, params: &[DecodedValue], globals: &str) -> Result<u64> {
300 let client = self.checkout().await?;
301 set_globals_on(&client, globals, false).await?;
302 execute_typed_on(&client, sql, params).await
303 }
304
305 pub async fn execute_typed(&self, sql: &str, params: &[DecodedValue]) -> Result<u64> {
310 let client = self.checkout().await?;
311 execute_typed_on(&client, sql, params).await
312 }
313
314 pub async fn query_explain(&self, sql: &str, params: &[DecodedValue]) -> Result<String> {
321 let client = self.checkout().await?;
322 query_explain_on(&client, sql, params).await
323 }
324
325 pub async fn begin(&self, isolation: &str) -> Result<PgTransaction> {
333 let client = self.checkout().await?;
334 let level = match isolation {
335 "read_uncommitted" => "READ UNCOMMITTED",
336 "read_committed" => "READ COMMITTED",
337 "repeatable_read" => "REPEATABLE READ",
338 "serializable" => "SERIALIZABLE",
339 other => return Err(Error::message(format!("unknown isolation level: {other:?}"))),
340 };
341 client.batch_execute(&format!("BEGIN ISOLATION LEVEL {level}")).await?;
342 Ok(PgTransaction {
343 client,
344 types: self.types.clone(),
345 })
346 }
347
348 pub async fn begin_default(&self) -> Result<PgTransaction> {
354 let client = self.checkout().await?;
355 client.batch_execute("BEGIN").await?;
356 Ok(PgTransaction {
357 client,
358 types: self.types.clone(),
359 })
360 }
361
362 pub async fn batch_execute(&self, sql: &str) -> Result<()> {
370 let client = self.checkout().await?;
371 client.batch_execute(sql).await?;
372 Ok(())
373 }
374
375 pub async fn connection(&self) -> Result<PgConnection> {
385 let client = self.checkout().await?;
386 Ok(PgConnection {
387 client,
388 types: self.types.clone(),
389 })
390 }
391}
392
393#[derive(Debug)]
396pub struct PgConnection {
397 client: deadpool_postgres::Object,
398 types: ExtensionOids,
399}
400
401impl PgConnection {
402 pub fn types(&self) -> &ExtensionOids {
404 &self.types
405 }
406
407 pub async fn query_typed(
408 &self,
409 sql: &str,
410 params: &[DecodedValue],
411 ext: &ExtensionOids,
412 ) -> Result<Vec<DecodedValue>> {
413 query_typed_on(&self.client, sql, params, ext).await
414 }
415
416 pub async fn execute_typed(&self, sql: &str, params: &[DecodedValue]) -> Result<u64> {
417 execute_typed_on(&self.client, sql, params).await
418 }
419
420 pub async fn batch_execute(&self, sql: &str) -> Result<()> {
421 self.client.batch_execute(sql).await?;
422 Ok(())
423 }
424}
425
426async fn query_with_stmt(
436 client: &tokio_postgres::Client,
437 stmt: &tokio_postgres::Statement,
438 params: &[DecodedValue],
439 ext: &ExtensionOids,
440) -> Result<Vec<DecodedValue>> {
441 let bound: Vec<BoundParam<'_>> = params.iter().map(BoundParam).collect();
442 let param_refs: Vec<&(dyn postgres_types::ToSql + Sync)> =
443 bound.iter().map(|p| p as &(dyn postgres_types::ToSql + Sync)).collect();
444 let rows = client.query(stmt, ¶m_refs).await?;
445 rows.iter().map(|row| decode_result_column(row, ext)).collect()
446}
447
448async fn query_named_with_stmt(
452 client: &tokio_postgres::Client,
453 stmt: &tokio_postgres::Statement,
454 params: &[DecodedValue],
455 ext: &ExtensionOids,
456) -> Result<Vec<DecodedValue>> {
457 let bound: Vec<BoundParam<'_>> = params.iter().map(BoundParam).collect();
458 let param_refs: Vec<&(dyn postgres_types::ToSql + Sync)> =
459 bound.iter().map(|p| p as &(dyn postgres_types::ToSql + Sync)).collect();
460 let rows = client.query(stmt, ¶m_refs).await?;
461 rows.iter().map(|row| decode_row_named(row, ext)).collect()
462}
463
464async fn execute_with_stmt(
465 client: &tokio_postgres::Client,
466 stmt: &tokio_postgres::Statement,
467 params: &[DecodedValue],
468) -> Result<u64> {
469 let bound: Vec<BoundParam<'_>> = params.iter().map(BoundParam).collect();
470 let param_refs: Vec<&(dyn postgres_types::ToSql + Sync)> =
471 bound.iter().map(|p| p as &(dyn postgres_types::ToSql + Sync)).collect();
472 Ok(client.execute(stmt, ¶m_refs).await?)
473}
474
475pub(crate) async fn query_typed_on(
486 client: &deadpool_postgres::Object,
487 sql: &str,
488 params: &[DecodedValue],
489 ext: &ExtensionOids,
490) -> Result<Vec<DecodedValue>> {
491 let stmt = client.prepare_cached(sql).await?;
492 query_with_stmt(client, &stmt, params, ext).await
493}
494
495pub(crate) async fn query_typed_named_on(
497 client: &deadpool_postgres::Object,
498 sql: &str,
499 params: &[DecodedValue],
500 ext: &ExtensionOids,
501) -> Result<Vec<DecodedValue>> {
502 let stmt = client.prepare_cached(sql).await?;
503 query_named_with_stmt(client, &stmt, params, ext).await
504}
505
506async fn set_globals_on(client: &deadpool_postgres::Object, globals: &str, local: bool) -> Result<()> {
509 let sql = if local {
510 "SELECT set_config('pylon.globals', $1, true)"
511 } else {
512 "SELECT set_config('pylon.globals', $1, false)"
513 };
514 execute_typed_on(client, sql, &[DecodedValue::Str(globals.to_string())]).await?;
515 Ok(())
516}
517
518pub(crate) async fn execute_typed_on(
520 client: &deadpool_postgres::Object,
521 sql: &str,
522 params: &[DecodedValue],
523) -> Result<u64> {
524 let stmt = client.prepare_cached(sql).await?;
525 execute_with_stmt(client, &stmt, params).await
526}
527
528pub(crate) async fn query_typed_on_raw(
533 client: &tokio_postgres::Client,
534 sql: &str,
535 params: &[DecodedValue],
536 ext: &ExtensionOids,
537) -> Result<Vec<DecodedValue>> {
538 let stmt = client.prepare(sql).await?;
539 query_with_stmt(client, &stmt, params, ext).await
540}
541
542pub(crate) async fn query_typed_named_on_raw(
543 client: &tokio_postgres::Client,
544 sql: &str,
545 params: &[DecodedValue],
546 ext: &ExtensionOids,
547) -> Result<Vec<DecodedValue>> {
548 let stmt = client.prepare(sql).await?;
549 query_named_with_stmt(client, &stmt, params, ext).await
550}
551
552pub(crate) async fn execute_typed_on_raw(
553 client: &tokio_postgres::Client,
554 sql: &str,
555 params: &[DecodedValue],
556) -> Result<u64> {
557 let stmt = client.prepare(sql).await?;
558 execute_with_stmt(client, &stmt, params).await
559}
560
561pub(crate) async fn query_explain_on(
567 client: &deadpool_postgres::Object,
568 sql: &str,
569 params: &[DecodedValue],
570) -> Result<String> {
571 let wrapped = format!("EXPLAIN (ANALYZE, FORMAT JSON, VERBOSE) {sql}");
572 let stmt = client.prepare_cached(&wrapped).await?;
573 let bound: Vec<BoundParam<'_>> = params.iter().map(BoundParam).collect();
574 let param_refs: Vec<&(dyn postgres_types::ToSql + Sync)> =
575 bound.iter().map(|p| p as &(dyn postgres_types::ToSql + Sync)).collect();
576 let rows = client.query(&stmt, ¶m_refs).await?;
577 let row = rows
578 .into_iter()
579 .next()
580 .ok_or_else(|| Error::message("EXPLAIN produced no output row".to_string()))?;
581 let RawBytes(bytes) = row.try_get::<_, RawBytes<'_>>(0)?;
582 Ok(String::from_utf8_lossy(bytes).into_owned())
583}
584
585#[derive(Debug)]
597pub struct PgTransaction {
598 client: deadpool_postgres::Object,
599 types: ExtensionOids,
600}
601
602impl PgTransaction {
603 pub fn types(&self) -> &ExtensionOids {
605 &self.types
606 }
607
608 pub async fn query_typed_with_globals(
611 &self,
612 sql: &str,
613 params: &[DecodedValue],
614 ext: &ExtensionOids,
615 globals: &str,
616 ) -> Result<Vec<DecodedValue>> {
617 set_globals_on(&self.client, globals, true).await?;
618 query_typed_on(&self.client, sql, params, ext).await
619 }
620
621 pub async fn execute_typed_with_globals(&self, sql: &str, params: &[DecodedValue], globals: &str) -> Result<u64> {
623 set_globals_on(&self.client, globals, true).await?;
624 execute_typed_on(&self.client, sql, params).await
625 }
626
627 pub async fn query_typed(
628 &self,
629 sql: &str,
630 params: &[DecodedValue],
631 ext: &ExtensionOids,
632 ) -> Result<Vec<DecodedValue>> {
633 query_typed_on(&self.client, sql, params, ext).await
634 }
635
636 pub async fn execute_typed(&self, sql: &str, params: &[DecodedValue]) -> Result<u64> {
637 execute_typed_on(&self.client, sql, params).await
638 }
639
640 pub async fn batch_execute(&self, sql: &str) -> Result<()> {
643 self.client.batch_execute(sql).await?;
644 Ok(())
645 }
646
647 pub async fn savepoint(&self, name: &str) -> Result<()> {
653 self.client
654 .batch_execute(&format!("SAVEPOINT {}", listener::quote_ident(name)))
655 .await?;
656 Ok(())
657 }
658
659 pub async fn release_savepoint(&self, name: &str) -> Result<()> {
660 self.client
661 .batch_execute(&format!("RELEASE SAVEPOINT {}", listener::quote_ident(name)))
662 .await?;
663 Ok(())
664 }
665
666 pub async fn rollback_to_savepoint(&self, name: &str) -> Result<()> {
667 self.client
668 .batch_execute(&format!("ROLLBACK TO SAVEPOINT {}", listener::quote_ident(name)))
669 .await?;
670 Ok(())
671 }
672
673 pub async fn commit(self) -> Result<()> {
678 match self.client.batch_execute("COMMIT").await {
679 Ok(()) => Ok(()),
680 Err(e) => {
681 let _ = self.client.batch_execute("ROLLBACK").await;
682 Err(e.into())
683 }
684 }
685 }
686
687 pub async fn rollback(self) -> Result<()> {
688 self.client.batch_execute("ROLLBACK").await?;
689 Ok(())
690 }
691}
692
693fn session_config(dsn: &str) -> Result<tokio_postgres::Config> {
712 let mut config: tokio_postgres::Config = dsn.parse()?;
713 let pinned = "-c timezone=UTC -c intervalstyle=iso_8601";
714 let options = match config.get_options() {
715 Some(existing) => format!("{existing} {pinned}"),
716 None => pinned.to_string(),
717 };
718 config.options(options);
719 Ok(config)
720}
721
722pub(crate) async fn discover_types(client: &tokio_postgres::Client) -> Result<ExtensionOids> {
726 let rows = client.query(wire::TYPE_DISCOVERY_SQL, &[]).await?;
727 Ok(ExtensionOids::from_discovery_rows(rows.iter().map(|r| {
728 let oid: i64 = r.get(0);
729 let typtype: String = r.get(1);
730 let base_oid: i64 = r.get(2);
731 let typname: String = r.get(3);
732 (oid as u32, typtype, base_oid as u32, typname)
733 })))
734}
735
736fn decode_result_column(row: &tokio_postgres::Row, ext: &ExtensionOids) -> Result<DecodedValue> {
737 let oid = row.columns()[0].type_().oid();
738 match row.try_get::<_, Option<RawBytes>>(0)? {
739 None => Ok(DecodedValue::Null),
740 Some(RawBytes(bytes)) => wire::decode_value(oid, bytes, ext),
741 }
742}
743
744fn decode_row_named(row: &tokio_postgres::Row, ext: &ExtensionOids) -> Result<DecodedValue> {
752 let mut fields = Vec::with_capacity(row.columns().len());
753 for (i, col) in row.columns().iter().enumerate() {
754 let oid = col.type_().oid();
755 let value = match row.try_get::<_, Option<RawBytes>>(i)? {
756 None => DecodedValue::Null,
757 Some(RawBytes(bytes)) => wire::decode_value(oid, bytes, ext)?,
758 };
759 fields.push((col.name().to_string(), value));
760 }
761 Ok(DecodedValue::Object(fields))
762}
763
764#[cfg(test)]
765mod tests {
766 use super::*;
767
768 fn test_dsn() -> String {
773 std::env::var("PYLON_PGCON_TEST_DSN").expect("PYLON_PGCON_TEST_DSN must be set to run live-Postgres tests")
774 }
775
776 #[tokio::test]
777 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
778 async fn connects_and_round_trips_a_scalar_query() {
779 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
780 let rows = pool.query_raw("SELECT 1 + 1").await.unwrap();
781 assert_eq!(rows.len(), 1);
782 let value: i32 = rows[0].get(0);
783 assert_eq!(value, 2);
784 }
785
786 #[tokio::test]
787 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
788 async fn pool_is_reused_across_multiple_queries() {
789 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
790 for i in 0..5 {
791 let rows = pool.query_raw(&format!("SELECT {i}")).await.unwrap();
792 let value: i32 = rows[0].get(0);
793 assert_eq!(value, i);
794 }
795 }
796
797 #[tokio::test]
798 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
799 async fn invalid_dsn_fails_to_connect() {
800 let result = PgPool::connect("not-a-valid-dsn", 5).await;
801 assert!(result.is_err());
802 }
803
804 #[tokio::test]
805 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
806 async fn connect_fails_eagerly_against_a_nonexistent_database() {
807 let dsn = test_dsn();
818 let (prefix, _db) = dsn.rsplit_once('/').expect("DSN must have a database path segment");
819 let bad_dsn = format!("{prefix}/pgcon_definitely_does_not_exist");
820 let result = PgPool::connect(&bad_dsn, 5).await;
821 assert!(result.is_err());
822 }
823
824 #[tokio::test]
825 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
826 async fn bad_sql_returns_an_error_not_a_panic() {
827 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
828 let result = pool.query_raw("SELECT this is not valid sql").await;
829 assert!(result.is_err());
830 }
831
832 #[tokio::test]
841 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
842 async fn decodes_a_bare_scalar_result_column() {
843 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
844 let rows = pool
845 .query_composite("SELECT 42::int8 AS result", &ExtensionOids::default())
846 .await
847 .unwrap();
848 assert_eq!(rows, vec![DecodedValue::I64(42)]);
849 }
850
851 #[tokio::test]
852 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
853 async fn decodes_a_composite_matching_pylon_cores_own_emission_shape() {
854 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
857 let sql = "SELECT ('Person'::text, 'Alice'::text, 30::int8, NULL::text) AS result";
858 let rows = pool.query_composite(sql, &ExtensionOids::default()).await.unwrap();
859 assert_eq!(
860 rows,
861 vec![DecodedValue::Composite(vec![
862 DecodedValue::Str("Person".into()),
863 DecodedValue::Str("Alice".into()),
864 DecodedValue::I64(30),
865 DecodedValue::Null,
866 ])]
867 );
868 }
869
870 #[tokio::test]
871 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
872 async fn decodes_nested_composite_and_array_of_composite_for_real() {
873 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
874 let sql = "SELECT (\
875 'Product'::text, \
876 ROW('Tag'::text, 'sale'::text), \
877 ARRAY[ROW(1::int8), ROW(2::int8)]::record[]\
878 ) AS result";
879 let rows = pool.query_composite(sql, &ExtensionOids::default()).await.unwrap();
880 assert_eq!(
881 rows,
882 vec![DecodedValue::Composite(vec![
883 DecodedValue::Str("Product".into()),
884 DecodedValue::Composite(vec![DecodedValue::Str("Tag".into()), DecodedValue::Str("sale".into())]),
885 DecodedValue::Array(vec![
886 DecodedValue::Composite(vec![DecodedValue::I64(1)]),
887 DecodedValue::Composite(vec![DecodedValue::I64(2)]),
888 ]),
889 ])]
890 );
891 }
892
893 #[tokio::test]
894 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
895 async fn decodes_array_of_text_for_real() {
896 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
897 let sql = "SELECT (ARRAY['a', 'b', NULL]::text[]) AS result";
898 let rows = pool.query_composite(sql, &ExtensionOids::default()).await.unwrap();
899 assert_eq!(
900 rows,
901 vec![DecodedValue::Array(vec![
902 DecodedValue::Str("a".into()),
903 DecodedValue::Str("b".into()),
904 DecodedValue::Null,
905 ])]
906 );
907 }
908
909 #[tokio::test]
910 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
911 async fn decodes_numeric_and_jsonb_and_uuid_for_real() {
912 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
913 let sql = "SELECT (\
914 12.50::numeric, \
915 '{\"a\": 1, \"b\": [1,2]}'::jsonb, \
916 '11111111-1111-1111-1111-111111111111'::uuid\
917 ) AS result";
918 let rows = pool.query_composite(sql, &ExtensionOids::default()).await.unwrap();
919 let DecodedValue::Composite(fields) = &rows[0] else {
920 panic!("expected Composite")
921 };
922 assert_eq!(fields[0], DecodedValue::Decimal("12.50".to_string()));
923 assert_eq!(
924 fields[1],
925 DecodedValue::Object(vec![
926 ("a".into(), DecodedValue::I64(1)),
927 (
928 "b".into(),
929 DecodedValue::Array(vec![DecodedValue::I64(1), DecodedValue::I64(2)])
930 ),
931 ])
932 );
933 assert_eq!(fields[2], DecodedValue::Uuid([0x11; 16]));
934 }
935
936 #[tokio::test]
937 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
938 async fn decodes_bytea_for_real() {
939 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
940 let rows = pool
941 .query_composite("SELECT '\\xdeadbeef'::bytea AS result", &ExtensionOids::default())
942 .await
943 .unwrap();
944 assert_eq!(rows, vec![DecodedValue::Bytes(vec![0xde, 0xad, 0xbe, 0xef])]);
945 }
946
947 #[tokio::test]
948 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
949 async fn decodes_enum_cast_to_text_for_real() {
950 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
954 let sql = "DO $$ BEGIN CREATE TYPE pgcon_test_enum AS ENUM ('a', 'b'); \
955 EXCEPTION WHEN duplicate_object THEN NULL; END $$;";
956 pool.query_raw(sql).await.ok();
957 let rows = pool
958 .query_composite(
959 "SELECT ('a'::pgcon_test_enum::text) AS result",
960 &ExtensionOids::default(),
961 )
962 .await
963 .unwrap();
964 assert_eq!(rows, vec![DecodedValue::Str("a".to_string())]);
965 }
966
967 #[tokio::test]
968 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
969 async fn decodes_a_real_pgvector_value_through_connect_time_discovery() {
970 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
971 if pool.query_raw("CREATE EXTENSION IF NOT EXISTS vector").await.is_err() {
972 eprintln!("skipping: pgvector not installable on this server");
973 return;
974 }
975 let mut pool = pool;
977 pool.refresh_types().await.unwrap();
978 assert!(
979 pool.types().vector.is_some(),
980 "discovery should have found the vector OID"
981 );
982
983 let rows = pool
985 .query_composite("SELECT ('doc', '[1.5,2.5]'::vector) AS result", pool.types())
986 .await
987 .unwrap();
988
989 let DecodedValue::Composite(fields) = &rows[0] else {
990 panic!("expected Composite, got {:?}", rows[0])
991 };
992 assert_eq!(fields[0], DecodedValue::Str("doc".to_string()));
993 assert_eq!(
994 fields[1],
995 DecodedValue::Array(vec![DecodedValue::F64(1.5), DecodedValue::F64(2.5)]),
996 "a vector must decode to its floats, not to mojibake or an error"
997 );
998 }
999
1000 #[tokio::test]
1001 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1002 async fn decodes_a_real_enum_and_domain_through_connect_time_discovery() {
1003 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1004 pool.query_raw(
1005 "DO $$ BEGIN CREATE TYPE pgcon_disc_enum AS ENUM ('x', 'y'); \
1006 EXCEPTION WHEN duplicate_object THEN NULL; END $$;",
1007 )
1008 .await
1009 .ok();
1010 pool.query_raw(
1011 "DO $$ BEGIN CREATE DOMAIN pgcon_disc_domain AS int8 CHECK (VALUE > 0); \
1012 EXCEPTION WHEN duplicate_object THEN NULL; END $$;",
1013 )
1014 .await
1015 .ok();
1016
1017 let mut pool = pool;
1018 pool.refresh_types().await.unwrap();
1019
1020 let rows = pool
1023 .query_composite(
1024 "SELECT ('x'::pgcon_disc_enum, 42::pgcon_disc_domain) AS result",
1025 pool.types(),
1026 )
1027 .await
1028 .unwrap();
1029
1030 let DecodedValue::Composite(fields) = &rows[0] else {
1031 panic!("expected Composite, got {:?}", rows[0])
1032 };
1033 assert_eq!(fields[0], DecodedValue::Str("x".to_string()));
1034 assert_eq!(
1035 fields[1],
1036 DecodedValue::I64(42),
1037 "a domain must decode as its base type, not as text"
1038 );
1039 }
1040
1041 async fn round_trip(pool: &PgPool, pg_type: &str, param: DecodedValue) -> DecodedValue {
1049 let sql = format!("SELECT ($1::{pg_type}) AS result");
1050 let rows = pool
1051 .query_typed(&sql, &[param], &ExtensionOids::default())
1052 .await
1053 .unwrap();
1054 rows.into_iter().next().unwrap()
1055 }
1056
1057 #[tokio::test]
1060 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1061 async fn query_explain_returns_parseable_json_with_a_plan_node() {
1062 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1063 let raw = pool.query_explain("SELECT 1 + 1", &[]).await.unwrap();
1064 let parsed: serde_json::Value = serde_json::from_str(&raw).unwrap();
1065 assert!(parsed[0]["Plan"]["Node Type"].is_string());
1066 assert!(parsed[0]["Plan"]["Actual Total Time"].is_number());
1068 }
1069
1070 #[tokio::test]
1071 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1072 async fn query_explain_binds_params_the_same_way_query_typed_does() {
1073 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1074 let raw = pool
1075 .query_explain("SELECT $1::int8 + 1", &[DecodedValue::I64(41)])
1076 .await
1077 .unwrap();
1078 let parsed: serde_json::Value = serde_json::from_str(&raw).unwrap();
1079 assert!(parsed[0]["Plan"]["Node Type"].is_string());
1080 }
1081
1082 #[tokio::test]
1083 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1084 async fn round_trips_bool_param() {
1085 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1086 assert_eq!(
1087 round_trip(&pool, "bool", DecodedValue::Bool(true)).await,
1088 DecodedValue::Bool(true)
1089 );
1090 assert_eq!(
1091 round_trip(&pool, "bool", DecodedValue::Bool(false)).await,
1092 DecodedValue::Bool(false)
1093 );
1094 }
1095
1096 #[tokio::test]
1097 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1098 async fn round_trips_integer_params_at_every_width() {
1099 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1100 assert_eq!(
1101 round_trip(&pool, "int2", DecodedValue::I64(30)).await,
1102 DecodedValue::I64(30)
1103 );
1104 assert_eq!(
1105 round_trip(&pool, "int4", DecodedValue::I64(70_000)).await,
1106 DecodedValue::I64(70_000)
1107 );
1108 assert_eq!(
1109 round_trip(&pool, "int8", DecodedValue::I64(9_223_372_036_854_775_807)).await,
1110 DecodedValue::I64(9_223_372_036_854_775_807)
1111 );
1112 assert_eq!(
1113 round_trip(&pool, "int8", DecodedValue::I64(-1)).await,
1114 DecodedValue::I64(-1)
1115 );
1116 }
1117
1118 #[tokio::test]
1119 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1120 async fn round_trips_float_params() {
1121 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1122 assert_eq!(
1123 round_trip(&pool, "float4", DecodedValue::F64(1.5)).await,
1124 DecodedValue::F64(1.5)
1125 );
1126 assert_eq!(
1127 round_trip(&pool, "float8", DecodedValue::F64(2.25)).await,
1128 DecodedValue::F64(2.25)
1129 );
1130 }
1131
1132 #[tokio::test]
1133 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1134 async fn round_trips_text_param() {
1135 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1136 assert_eq!(
1137 round_trip(&pool, "text", DecodedValue::Str("héllo 🎉".to_string())).await,
1138 DecodedValue::Str("héllo 🎉".to_string())
1139 );
1140 }
1141
1142 #[tokio::test]
1143 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1144 async fn round_trips_bytea_param() {
1145 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1146 assert_eq!(
1147 round_trip(&pool, "bytea", DecodedValue::Bytes(vec![1, 2, 3, 255])).await,
1148 DecodedValue::Bytes(vec![1, 2, 3, 255])
1149 );
1150 }
1151
1152 #[tokio::test]
1153 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1154 async fn round_trips_uuid_param() {
1155 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1156 let bytes = [0x11u8; 16];
1157 assert_eq!(
1158 round_trip(&pool, "uuid", DecodedValue::Uuid(bytes)).await,
1159 DecodedValue::Uuid(bytes)
1160 );
1161 }
1162
1163 #[tokio::test]
1164 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1165 async fn binds_a_plain_string_as_a_uuid_param() {
1166 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1174 let as_string = DecodedValue::Str("11111111-1111-1111-1111-111111111111".to_string());
1175 assert_eq!(
1176 round_trip(&pool, "uuid", as_string).await,
1177 DecodedValue::Uuid([0x11; 16])
1178 );
1179 }
1180
1181 #[tokio::test]
1182 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1183 async fn binds_a_plain_string_as_a_jsonb_param() {
1184 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1190 let as_string = DecodedValue::Str(r#"{"a":1,"b":[1,2]}"#.to_string());
1191 assert_eq!(
1192 round_trip(&pool, "jsonb", as_string).await,
1193 DecodedValue::Object(vec![
1194 ("a".into(), DecodedValue::I64(1)),
1195 (
1196 "b".into(),
1197 DecodedValue::Array(vec![DecodedValue::I64(1), DecodedValue::I64(2)])
1198 ),
1199 ])
1200 );
1201 }
1202
1203 #[tokio::test]
1204 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1205 async fn round_trips_numeric_param() {
1206 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1207 assert_eq!(
1208 round_trip(&pool, "numeric", DecodedValue::Decimal("12.50".to_string())).await,
1209 DecodedValue::Decimal("12.50".to_string())
1210 );
1211 assert_eq!(
1212 round_trip(&pool, "numeric", DecodedValue::Decimal("-9999.001".to_string())).await,
1213 DecodedValue::Decimal("-9999.001".to_string())
1214 );
1215
1216 for text in [
1221 "0.00000039999999999999998189924473035450347424557548947632312774658203125",
1222 "0.00000012222222222222222222222222222222222222222",
1223 "12222222222222222222222222222222000000000",
1224 "100000000000000000000000000000",
1225 "123456789012345678901234567890.123456789012345678901234567890",
1226 ] {
1227 assert_eq!(
1228 round_trip(&pool, "numeric", DecodedValue::Decimal(text.to_string())).await,
1229 DecodedValue::Decimal(text.to_string()),
1230 "{text}"
1231 );
1232 }
1233
1234 assert_eq!(
1237 round_trip(&pool, "numeric", DecodedValue::Str("0.0000004".to_string())).await,
1238 DecodedValue::Decimal("0.0000004".to_string())
1239 );
1240 assert_eq!(
1241 round_trip(&pool, "numeric", DecodedValue::I64(-12345)).await,
1242 DecodedValue::Decimal("-12345".to_string())
1243 );
1244 assert_eq!(
1245 round_trip(&pool, "numeric", DecodedValue::F64(0.1)).await,
1246 DecodedValue::Decimal("0.1".to_string())
1247 );
1248 }
1249
1250 #[tokio::test]
1251 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1252 async fn round_trips_null_param() {
1253 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1254 assert_eq!(round_trip(&pool, "int8", DecodedValue::Null).await, DecodedValue::Null);
1255 assert_eq!(round_trip(&pool, "text", DecodedValue::Null).await, DecodedValue::Null);
1256 }
1257
1258 #[tokio::test]
1259 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1260 async fn round_trips_array_param() {
1261 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1262 let param = DecodedValue::Array(vec![
1263 DecodedValue::Str("a".into()),
1264 DecodedValue::Str("b".into()),
1265 DecodedValue::Null,
1266 ]);
1267 assert_eq!(round_trip(&pool, "text[]", param.clone()).await, param);
1268 }
1269
1270 #[tokio::test]
1271 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1272 async fn round_trips_int_array_param() {
1273 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1274 let param = DecodedValue::Array(vec![DecodedValue::I64(1), DecodedValue::I64(2), DecodedValue::I64(3)]);
1275 assert_eq!(round_trip(&pool, "int8[]", param.clone()).await, param);
1276 }
1277
1278 #[tokio::test]
1279 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1280 async fn round_trips_jsonb_object_param() {
1281 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1282 let param = DecodedValue::Object(vec![
1283 ("a".into(), DecodedValue::I64(1)),
1284 ("b".into(), DecodedValue::Str("two".into())),
1285 (
1286 "c".into(),
1287 DecodedValue::Array(vec![DecodedValue::I64(1), DecodedValue::I64(2)]),
1288 ),
1289 ]);
1290 assert_eq!(round_trip(&pool, "jsonb", param.clone()).await, param);
1291 }
1292
1293 #[tokio::test]
1294 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1295 async fn query_typed_matches_pylon_cores_own_param_binding_convention() {
1296 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1299 let sql = "SELECT ($1::text, $2::int8, $3::bool) AS result";
1300 let params = vec![
1301 DecodedValue::Str("Alice".into()),
1302 DecodedValue::I64(30),
1303 DecodedValue::Bool(true),
1304 ];
1305 let rows = pool.query_typed(sql, ¶ms, &ExtensionOids::default()).await.unwrap();
1306 assert_eq!(
1307 rows,
1308 vec![DecodedValue::Composite(vec![
1309 DecodedValue::Str("Alice".into()),
1310 DecodedValue::I64(30),
1311 DecodedValue::Bool(true),
1312 ])]
1313 );
1314 }
1315
1316 #[tokio::test]
1317 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1318 async fn wrong_param_count_returns_an_error_not_a_panic() {
1319 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1320 let result = pool
1321 .query_typed(
1322 "SELECT $1::int8, $2::int8",
1323 &[DecodedValue::I64(1)],
1324 &ExtensionOids::default(),
1325 )
1326 .await;
1327 assert!(result.is_err());
1328 }
1329
1330 #[tokio::test]
1331 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1332 async fn execute_typed_runs_a_mutation_and_reports_affected_rows() {
1333 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1334 pool.query_raw("CREATE TEMP TABLE IF NOT EXISTS pgcon_execute_test (id int8, name text)")
1335 .await
1336 .unwrap();
1337
1338 let inserted = pool
1339 .execute_typed(
1340 "INSERT INTO pgcon_execute_test (id, name) VALUES ($1::int8, $2::text)",
1341 &[DecodedValue::I64(1), DecodedValue::Str("alice".into())],
1342 )
1343 .await
1344 .unwrap();
1345 assert_eq!(inserted, 1);
1346
1347 let updated = pool
1348 .execute_typed(
1349 "UPDATE pgcon_execute_test SET name = $1::text WHERE id = $2::int8",
1350 &[DecodedValue::Str("bob".into()), DecodedValue::I64(1)],
1351 )
1352 .await
1353 .unwrap();
1354 assert_eq!(updated, 1);
1355
1356 let rows = pool
1357 .query_composite(
1358 "SELECT (name) AS result FROM pgcon_execute_test",
1359 &ExtensionOids::default(),
1360 )
1361 .await
1362 .unwrap();
1363 assert_eq!(rows, vec![DecodedValue::Str("bob".to_string())]);
1364 }
1365
1366 #[tokio::test]
1374 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1375 async fn unique_violation_reports_23505() {
1376 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1377 pool.query_raw("CREATE TEMP TABLE pgcon_unique_test (id int8 PRIMARY KEY)")
1378 .await
1379 .unwrap();
1380 pool.execute_typed(
1381 "INSERT INTO pgcon_unique_test (id) VALUES ($1::int8)",
1382 &[DecodedValue::I64(1)],
1383 )
1384 .await
1385 .unwrap();
1386
1387 let err = pool
1388 .execute_typed(
1389 "INSERT INTO pgcon_unique_test (id) VALUES ($1::int8)",
1390 &[DecodedValue::I64(1)],
1391 )
1392 .await
1393 .unwrap_err();
1394 assert_eq!(err.sqlstate(), Some(&tokio_postgres::error::SqlState::UNIQUE_VIOLATION));
1395 assert_eq!(err.sqlstate().unwrap().code(), "23505");
1396 }
1397
1398 #[tokio::test]
1399 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1400 async fn foreign_key_violation_reports_23503() {
1401 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1402 pool.query_raw("CREATE TEMP TABLE pgcon_fk_parent (id int8 PRIMARY KEY)")
1403 .await
1404 .unwrap();
1405 pool.query_raw("CREATE TEMP TABLE pgcon_fk_child (parent_id int8 REFERENCES pgcon_fk_parent(id))")
1406 .await
1407 .unwrap();
1408
1409 let err = pool
1410 .execute_typed(
1411 "INSERT INTO pgcon_fk_child (parent_id) VALUES ($1::int8)",
1412 &[DecodedValue::I64(999)],
1413 )
1414 .await
1415 .unwrap_err();
1416 assert_eq!(
1417 err.sqlstate(),
1418 Some(&tokio_postgres::error::SqlState::FOREIGN_KEY_VIOLATION)
1419 );
1420 }
1421
1422 #[tokio::test]
1423 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1424 async fn check_violation_reports_23514() {
1425 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1426 pool.query_raw("CREATE TEMP TABLE pgcon_check_test (age int8 CHECK (age >= 0))")
1427 .await
1428 .unwrap();
1429
1430 let err = pool
1431 .execute_typed(
1432 "INSERT INTO pgcon_check_test (age) VALUES ($1::int8)",
1433 &[DecodedValue::I64(-1)],
1434 )
1435 .await
1436 .unwrap_err();
1437 assert_eq!(err.sqlstate(), Some(&tokio_postgres::error::SqlState::CHECK_VIOLATION));
1438 assert_eq!(err.violated_table().map(|(_, table)| table), Some("pgcon_check_test"));
1441 assert_eq!(err.violated_scalar(), None);
1442 }
1443
1444 #[tokio::test]
1445 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1446 async fn domain_check_violation_reports_the_domain_name_not_the_constraint_name() {
1447 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1448 pool.query_raw(
1449 "DO $$ BEGIN CREATE DOMAIN pgcon_rating AS int8 CHECK (VALUE BETWEEN 1 AND 5); \
1450 EXCEPTION WHEN duplicate_object THEN NULL; END $$",
1451 )
1452 .await
1453 .unwrap();
1454 pool.query_raw("CREATE TEMP TABLE pgcon_domain_check_test (rating pgcon_rating)")
1455 .await
1456 .unwrap();
1457
1458 let err = pool
1459 .execute_typed(
1460 "INSERT INTO pgcon_domain_check_test (rating) VALUES ($1::pgcon_rating)",
1461 &[DecodedValue::I64(99)],
1462 )
1463 .await
1464 .unwrap_err();
1465 assert_eq!(err.sqlstate(), Some(&tokio_postgres::error::SqlState::CHECK_VIOLATION));
1466 assert_eq!(err.violated_scalar(), Some(("public", "pgcon_rating")));
1467 }
1468
1469 #[tokio::test]
1470 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1471 async fn syntax_error_has_no_sqlstate_matching_constraint_codes() {
1472 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1473 let err = pool.query_raw("SELECT this is not valid sql").await.unwrap_err();
1474 assert_ne!(err.sqlstate(), Some(&tokio_postgres::error::SqlState::UNIQUE_VIOLATION));
1475 }
1476
1477 #[tokio::test]
1478 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1479 async fn connection_pool_error_has_no_sqlstate() {
1480 let result = PgPool::connect("not-a-valid-dsn", 5).await;
1483 let err = result.unwrap_err();
1484 assert_eq!(err.sqlstate(), None);
1485 }
1486
1487 #[tokio::test]
1490 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1491 async fn committed_transaction_persists_its_writes() {
1492 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1493 pool.query_raw("CREATE TEMP TABLE pgcon_tx_commit_test (id int8 PRIMARY KEY)")
1494 .await
1495 .unwrap();
1496
1497 let tx = pool.begin("serializable").await.unwrap();
1498 tx.execute_typed(
1499 "INSERT INTO pgcon_tx_commit_test (id) VALUES ($1::int8)",
1500 &[DecodedValue::I64(1)],
1501 )
1502 .await
1503 .unwrap();
1504 tx.commit().await.unwrap();
1505
1506 let rows = pool
1507 .query_composite(
1508 "SELECT (id) AS result FROM pgcon_tx_commit_test",
1509 &ExtensionOids::default(),
1510 )
1511 .await
1512 .unwrap();
1513 assert_eq!(rows, vec![DecodedValue::I64(1)]);
1514 }
1515
1516 #[tokio::test]
1517 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1518 async fn rolled_back_transaction_discards_its_writes() {
1519 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1520 pool.query_raw("CREATE TEMP TABLE pgcon_tx_rollback_test (id int8 PRIMARY KEY)")
1521 .await
1522 .unwrap();
1523
1524 let tx = pool.begin("serializable").await.unwrap();
1525 tx.execute_typed(
1526 "INSERT INTO pgcon_tx_rollback_test (id) VALUES ($1::int8)",
1527 &[DecodedValue::I64(1)],
1528 )
1529 .await
1530 .unwrap();
1531 tx.rollback().await.unwrap();
1532
1533 let rows = pool
1534 .query_composite(
1535 "SELECT (id) AS result FROM pgcon_tx_rollback_test",
1536 &ExtensionOids::default(),
1537 )
1538 .await
1539 .unwrap();
1540 assert!(rows.is_empty());
1541 }
1542
1543 #[tokio::test]
1544 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1545 async fn begin_actually_sets_the_requested_isolation_level() {
1546 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1547 for (level, expected) in [
1548 ("read_committed", "read committed"),
1549 ("repeatable_read", "repeatable read"),
1550 ("serializable", "serializable"),
1551 ] {
1552 let tx = pool.begin(level).await.unwrap();
1553 let rows = tx
1554 .query_typed(
1555 "SELECT (current_setting('transaction_isolation')) AS result",
1556 &[],
1557 &ExtensionOids::default(),
1558 )
1559 .await
1560 .unwrap();
1561 assert_eq!(rows, vec![DecodedValue::Str(expected.to_string())]);
1562 tx.rollback().await.unwrap();
1563 }
1564 }
1565
1566 #[tokio::test]
1567 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1568 async fn begin_rejects_an_unknown_isolation_level() {
1569 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1570 let result = pool.begin("not_a_real_level").await;
1571 assert!(result.is_err());
1572 }
1573
1574 #[tokio::test]
1575 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1576 async fn a_pooled_connection_is_reusable_after_commit_and_after_rollback() {
1577 let pool = PgPool::connect(&test_dsn(), 1).await.unwrap();
1584
1585 let tx = pool.begin("serializable").await.unwrap();
1586 tx.commit().await.unwrap();
1587
1588 let tx = pool.begin("serializable").await.unwrap();
1589 tx.rollback().await.unwrap();
1590
1591 let rows = pool.query_raw("SELECT 1").await.unwrap();
1592 assert_eq!(rows.len(), 1);
1593 }
1594
1595 #[tokio::test]
1596 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1597 async fn failed_commit_leaves_the_connection_reusable() {
1598 let pool = PgPool::connect(&test_dsn(), 2).await.unwrap();
1608 pool.query_raw("DROP TABLE IF EXISTS pgcon_tx_failed_commit_test")
1609 .await
1610 .unwrap();
1611 pool.query_raw("CREATE TABLE pgcon_tx_failed_commit_test (class int8, value int8)")
1612 .await
1613 .unwrap();
1614 pool.execute_typed(
1615 "INSERT INTO pgcon_tx_failed_commit_test (class, value) VALUES ($1::int8, $2::int8), ($3::int8, $4::int8)",
1616 &[
1617 DecodedValue::I64(1),
1618 DecodedValue::I64(10),
1619 DecodedValue::I64(2),
1620 DecodedValue::I64(20),
1621 ],
1622 )
1623 .await
1624 .unwrap();
1625
1626 let tx1 = pool.begin("serializable").await.unwrap();
1627 let tx2 = pool.begin("serializable").await.unwrap();
1628
1629 tx1.query_typed(
1630 "SELECT (sum(value)) AS result FROM pgcon_tx_failed_commit_test WHERE class = 1::int8",
1631 &[],
1632 &ExtensionOids::default(),
1633 )
1634 .await
1635 .unwrap();
1636 tx2.query_typed(
1637 "SELECT (sum(value)) AS result FROM pgcon_tx_failed_commit_test WHERE class = 2::int8",
1638 &[],
1639 &ExtensionOids::default(),
1640 )
1641 .await
1642 .unwrap();
1643 tx1.execute_typed(
1644 "INSERT INTO pgcon_tx_failed_commit_test (class, value) VALUES (2::int8, $1::int8)",
1645 &[DecodedValue::I64(10)],
1646 )
1647 .await
1648 .unwrap();
1649 tx2.execute_typed(
1650 "INSERT INTO pgcon_tx_failed_commit_test (class, value) VALUES (1::int8, $1::int8)",
1651 &[DecodedValue::I64(20)],
1652 )
1653 .await
1654 .unwrap();
1655
1656 tx1.commit().await.unwrap();
1657 let commit_result = tx2.commit().await;
1658 assert!(commit_result.is_err());
1659
1660 for _ in 0..2 {
1663 let rows = pool.query_raw("SELECT 1").await.unwrap();
1664 assert_eq!(rows.len(), 1);
1665 }
1666 }
1667
1668 #[tokio::test]
1669 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1670 async fn serializable_transactions_conflict_with_40001() {
1671 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1679 pool.query_raw("DROP TABLE IF EXISTS pgcon_serialization_test")
1680 .await
1681 .unwrap();
1682 pool.query_raw("CREATE TABLE pgcon_serialization_test (class int8, value int8)")
1683 .await
1684 .unwrap();
1685 pool.execute_typed(
1686 "INSERT INTO pgcon_serialization_test (class, value) VALUES ($1::int8, $2::int8), ($3::int8, $4::int8)",
1687 &[
1688 DecodedValue::I64(1),
1689 DecodedValue::I64(10),
1690 DecodedValue::I64(2),
1691 DecodedValue::I64(20),
1692 ],
1693 )
1694 .await
1695 .unwrap();
1696
1697 let tx1 = pool.begin("serializable").await.unwrap();
1698 let tx2 = pool.begin("serializable").await.unwrap();
1699
1700 tx1.query_typed(
1701 "SELECT (sum(value)) AS result FROM pgcon_serialization_test WHERE class = 1::int8",
1702 &[],
1703 &ExtensionOids::default(),
1704 )
1705 .await
1706 .unwrap();
1707 tx2.query_typed(
1708 "SELECT (sum(value)) AS result FROM pgcon_serialization_test WHERE class = 2::int8",
1709 &[],
1710 &ExtensionOids::default(),
1711 )
1712 .await
1713 .unwrap();
1714
1715 tx1.execute_typed(
1716 "INSERT INTO pgcon_serialization_test (class, value) VALUES (2::int8, $1::int8)",
1717 &[DecodedValue::I64(10)],
1718 )
1719 .await
1720 .unwrap();
1721 tx2.execute_typed(
1722 "INSERT INTO pgcon_serialization_test (class, value) VALUES (1::int8, $1::int8)",
1723 &[DecodedValue::I64(20)],
1724 )
1725 .await
1726 .unwrap();
1727
1728 tx1.commit().await.unwrap();
1729 let err = tx2.commit().await.unwrap_err();
1730 assert_eq!(
1731 err.sqlstate(),
1732 Some(&tokio_postgres::error::SqlState::T_R_SERIALIZATION_FAILURE)
1733 );
1734 }
1735
1736 #[tokio::test]
1737 #[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
1738 async fn concurrent_transactions_deadlock_with_40p01() {
1739 let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
1745 pool.query_raw("DROP TABLE IF EXISTS pgcon_deadlock_test")
1746 .await
1747 .unwrap();
1748 pool.query_raw("CREATE TABLE pgcon_deadlock_test (id int8 PRIMARY KEY, value int8)")
1749 .await
1750 .unwrap();
1751 pool.execute_typed(
1752 "INSERT INTO pgcon_deadlock_test (id, value) VALUES ($1::int8, $2::int8), ($3::int8, $4::int8)",
1753 &[
1754 DecodedValue::I64(1),
1755 DecodedValue::I64(0),
1756 DecodedValue::I64(2),
1757 DecodedValue::I64(0),
1758 ],
1759 )
1760 .await
1761 .unwrap();
1762
1763 let tx1 = pool.begin("read_committed").await.unwrap();
1764 let tx2 = pool.begin("read_committed").await.unwrap();
1765
1766 tx1.execute_typed("UPDATE pgcon_deadlock_test SET value = 1::int8 WHERE id = 1::int8", &[])
1767 .await
1768 .unwrap();
1769 tx2.execute_typed("UPDATE pgcon_deadlock_test SET value = 2::int8 WHERE id = 2::int8", &[])
1770 .await
1771 .unwrap();
1772
1773 let (r1, r2) = tokio::join!(
1776 tx1.execute_typed("UPDATE pgcon_deadlock_test SET value = 3::int8 WHERE id = 2::int8", &[]),
1777 tx2.execute_typed("UPDATE pgcon_deadlock_test SET value = 4::int8 WHERE id = 1::int8", &[]),
1778 );
1779
1780 let results = [r1, r2];
1781 let deadlock_errors: Vec<_> = results
1782 .iter()
1783 .filter(|r| matches!(r, Err(e) if e.sqlstate() == Some(&tokio_postgres::error::SqlState::T_R_DEADLOCK_DETECTED)))
1784 .collect();
1785 assert_eq!(
1786 deadlock_errors.len(),
1787 1,
1788 "expected exactly one side to be aborted with 40P01, got {results:?}"
1789 );
1790 }
1791}
1792
1793#[cfg(test)]
1794mod error_message_tests {
1795 use super::*;
1796
1797 #[tokio::test]
1802 #[ignore = "requires a live database"]
1803 async fn a_server_error_renders_the_server_message() {
1804 let pool = PgPool::connect(&std::env::var("PYLON_PGCON_TEST_DSN").unwrap(), 2)
1805 .await
1806 .unwrap();
1807 let err = pool
1811 .query_typed("SELECT 1 FROM pylon_no_such_table", &[], pool.types())
1812 .await
1813 .unwrap_err();
1814 let rendered = err.to_string();
1815 assert!(
1816 rendered.contains("pylon_no_such_table"),
1817 "expected the server's own message, got {rendered:?}"
1818 );
1819 assert_ne!(rendered, "db error");
1820 }
1821}