pub mod error;
pub mod listener;
pub mod numeric;
pub mod wire;
pub use error::{Error, Result};
pub use listener::PgListener;
pub use wire::{ExtensionOids, decode_value};
use pylon_value::DecodedValue;
struct RawBytes<'a>(&'a [u8]);
impl<'a> postgres_types::FromSql<'a> for RawBytes<'a> {
fn from_sql(
_ty: &postgres_types::Type,
raw: &'a [u8],
) -> std::result::Result<Self, Box<dyn std::error::Error + Sync + Send>> {
Ok(RawBytes(raw))
}
fn accepts(_ty: &postgres_types::Type) -> bool {
true
}
}
#[derive(Debug)]
struct BoundParam<'a>(&'a DecodedValue);
impl postgres_types::ToSql for BoundParam<'_> {
fn to_sql(
&self,
ty: &postgres_types::Type,
out: &mut bytes::BytesMut,
) -> std::result::Result<postgres_types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
Ok(wire::encode_value(self.0, ty, out)?)
}
fn accepts(_ty: &postgres_types::Type) -> bool {
true
}
postgres_types::to_sql_checked!();
}
#[derive(Clone, Debug)]
pub struct PgPool {
pool: deadpool_postgres::Pool,
types: ExtensionOids,
name: String,
}
pub type PoolWaitObserver = fn(&str, std::time::Duration);
static POOL_WAIT_OBSERVER: std::sync::OnceLock<PoolWaitObserver> = std::sync::OnceLock::new();
pub fn set_pool_wait_observer(observer: PoolWaitObserver) {
let _ = POOL_WAIT_OBSERVER.set(observer);
}
pub struct PoolStatus {
pub size: usize,
pub available: usize,
pub waiting: usize,
pub max_size: usize,
}
impl PgPool {
pub async fn connect(dsn: &str, max_size: usize) -> Result<Self> {
let pg_config = session_config(dsn)?;
let manager = deadpool_postgres::Manager::new(pg_config, tokio_postgres::NoTls);
let pool = deadpool_postgres::Pool::builder(manager)
.max_size(max_size)
.create_timeout(Some(std::time::Duration::from_secs(10)))
.runtime(deadpool_postgres::Runtime::Tokio1)
.build()?;
let client = pool.get().await?;
let types = discover_types(&client).await?;
drop(client);
Ok(Self {
pool,
types,
name: "default".to_string(),
})
}
pub fn types(&self) -> &ExtensionOids {
&self.types
}
pub async fn refresh_types(&mut self) -> Result<()> {
let client = self.checkout().await?;
self.types = discover_types(&client).await?;
drop(client);
self.clear_statement_caches();
Ok(())
}
pub fn clear_statement_caches(&self) {
self.pool.manager().statement_caches.clear();
}
pub fn set_name(&mut self, name: impl Into<String>) {
self.name = name.into();
}
async fn checkout(&self) -> Result<deadpool_postgres::Object> {
let started = std::time::Instant::now();
let client = self.pool.get().await?;
if let Some(observe) = POOL_WAIT_OBSERVER.get() {
observe(&self.name, started.elapsed());
}
Ok(client)
}
pub fn status(&self) -> PoolStatus {
let s = self.pool.status();
PoolStatus {
size: s.size,
available: s.available,
waiting: s.waiting,
max_size: s.max_size,
}
}
pub async fn query_raw(&self, sql: &str) -> Result<Vec<tokio_postgres::Row>> {
let client = self.checkout().await?;
let rows = client.query(sql, &[]).await?;
Ok(rows)
}
pub async fn query_composite(&self, sql: &str, ext: &ExtensionOids) -> Result<Vec<DecodedValue>> {
let rows = self.query_raw(sql).await?;
rows.iter().map(|row| decode_result_column(row, ext)).collect()
}
pub async fn query_typed(
&self,
sql: &str,
params: &[DecodedValue],
ext: &ExtensionOids,
) -> Result<Vec<DecodedValue>> {
let client = self.checkout().await?;
query_typed_on(&client, sql, params, ext).await
}
pub async fn query_typed_named(
&self,
sql: &str,
params: &[DecodedValue],
ext: &ExtensionOids,
) -> Result<Vec<DecodedValue>> {
let client = self.checkout().await?;
query_typed_named_on(&client, sql, params, ext).await
}
pub async fn query_typed_with_globals(
&self,
sql: &str,
params: &[DecodedValue],
ext: &ExtensionOids,
globals: &str,
) -> Result<Vec<DecodedValue>> {
let client = self.checkout().await?;
set_globals_on(&client, globals, false).await?;
query_typed_on(&client, sql, params, ext).await
}
pub async fn execute_typed_with_globals(&self, sql: &str, params: &[DecodedValue], globals: &str) -> Result<u64> {
let client = self.checkout().await?;
set_globals_on(&client, globals, false).await?;
execute_typed_on(&client, sql, params).await
}
pub async fn execute_typed(&self, sql: &str, params: &[DecodedValue]) -> Result<u64> {
let client = self.checkout().await?;
execute_typed_on(&client, sql, params).await
}
pub async fn query_explain(&self, sql: &str, params: &[DecodedValue]) -> Result<String> {
let client = self.checkout().await?;
query_explain_on(&client, sql, params).await
}
pub async fn begin(&self, isolation: &str) -> Result<PgTransaction> {
let client = self.checkout().await?;
let level = match isolation {
"read_uncommitted" => "READ UNCOMMITTED",
"read_committed" => "READ COMMITTED",
"repeatable_read" => "REPEATABLE READ",
"serializable" => "SERIALIZABLE",
other => return Err(Error::message(format!("unknown isolation level: {other:?}"))),
};
client.batch_execute(&format!("BEGIN ISOLATION LEVEL {level}")).await?;
Ok(PgTransaction {
client,
types: self.types.clone(),
})
}
pub async fn begin_default(&self) -> Result<PgTransaction> {
let client = self.checkout().await?;
client.batch_execute("BEGIN").await?;
Ok(PgTransaction {
client,
types: self.types.clone(),
})
}
pub async fn batch_execute(&self, sql: &str) -> Result<()> {
let client = self.checkout().await?;
client.batch_execute(sql).await?;
Ok(())
}
pub async fn connection(&self) -> Result<PgConnection> {
let client = self.checkout().await?;
Ok(PgConnection {
client,
types: self.types.clone(),
})
}
}
#[derive(Debug)]
pub struct PgConnection {
client: deadpool_postgres::Object,
types: ExtensionOids,
}
impl PgConnection {
pub fn types(&self) -> &ExtensionOids {
&self.types
}
pub async fn query_typed(
&self,
sql: &str,
params: &[DecodedValue],
ext: &ExtensionOids,
) -> Result<Vec<DecodedValue>> {
query_typed_on(&self.client, sql, params, ext).await
}
pub async fn execute_typed(&self, sql: &str, params: &[DecodedValue]) -> Result<u64> {
execute_typed_on(&self.client, sql, params).await
}
pub async fn batch_execute(&self, sql: &str) -> Result<()> {
self.client.batch_execute(sql).await?;
Ok(())
}
}
async fn query_with_stmt(
client: &tokio_postgres::Client,
stmt: &tokio_postgres::Statement,
params: &[DecodedValue],
ext: &ExtensionOids,
) -> Result<Vec<DecodedValue>> {
let bound: Vec<BoundParam<'_>> = params.iter().map(BoundParam).collect();
let param_refs: Vec<&(dyn postgres_types::ToSql + Sync)> =
bound.iter().map(|p| p as &(dyn postgres_types::ToSql + Sync)).collect();
let rows = client.query(stmt, ¶m_refs).await?;
rows.iter().map(|row| decode_result_column(row, ext)).collect()
}
async fn query_named_with_stmt(
client: &tokio_postgres::Client,
stmt: &tokio_postgres::Statement,
params: &[DecodedValue],
ext: &ExtensionOids,
) -> Result<Vec<DecodedValue>> {
let bound: Vec<BoundParam<'_>> = params.iter().map(BoundParam).collect();
let param_refs: Vec<&(dyn postgres_types::ToSql + Sync)> =
bound.iter().map(|p| p as &(dyn postgres_types::ToSql + Sync)).collect();
let rows = client.query(stmt, ¶m_refs).await?;
rows.iter().map(|row| decode_row_named(row, ext)).collect()
}
async fn execute_with_stmt(
client: &tokio_postgres::Client,
stmt: &tokio_postgres::Statement,
params: &[DecodedValue],
) -> Result<u64> {
let bound: Vec<BoundParam<'_>> = params.iter().map(BoundParam).collect();
let param_refs: Vec<&(dyn postgres_types::ToSql + Sync)> =
bound.iter().map(|p| p as &(dyn postgres_types::ToSql + Sync)).collect();
Ok(client.execute(stmt, ¶m_refs).await?)
}
pub(crate) async fn query_typed_on(
client: &deadpool_postgres::Object,
sql: &str,
params: &[DecodedValue],
ext: &ExtensionOids,
) -> Result<Vec<DecodedValue>> {
let stmt = client.prepare_cached(sql).await?;
query_with_stmt(client, &stmt, params, ext).await
}
pub(crate) async fn query_typed_named_on(
client: &deadpool_postgres::Object,
sql: &str,
params: &[DecodedValue],
ext: &ExtensionOids,
) -> Result<Vec<DecodedValue>> {
let stmt = client.prepare_cached(sql).await?;
query_named_with_stmt(client, &stmt, params, ext).await
}
async fn set_globals_on(client: &deadpool_postgres::Object, globals: &str, local: bool) -> Result<()> {
let sql = if local {
"SELECT set_config('pylon.globals', $1, true)"
} else {
"SELECT set_config('pylon.globals', $1, false)"
};
execute_typed_on(client, sql, &[DecodedValue::Str(globals.to_string())]).await?;
Ok(())
}
pub(crate) async fn execute_typed_on(
client: &deadpool_postgres::Object,
sql: &str,
params: &[DecodedValue],
) -> Result<u64> {
let stmt = client.prepare_cached(sql).await?;
execute_with_stmt(client, &stmt, params).await
}
pub(crate) async fn query_typed_on_raw(
client: &tokio_postgres::Client,
sql: &str,
params: &[DecodedValue],
ext: &ExtensionOids,
) -> Result<Vec<DecodedValue>> {
let stmt = client.prepare(sql).await?;
query_with_stmt(client, &stmt, params, ext).await
}
pub(crate) async fn query_typed_named_on_raw(
client: &tokio_postgres::Client,
sql: &str,
params: &[DecodedValue],
ext: &ExtensionOids,
) -> Result<Vec<DecodedValue>> {
let stmt = client.prepare(sql).await?;
query_named_with_stmt(client, &stmt, params, ext).await
}
pub(crate) async fn execute_typed_on_raw(
client: &tokio_postgres::Client,
sql: &str,
params: &[DecodedValue],
) -> Result<u64> {
let stmt = client.prepare(sql).await?;
execute_with_stmt(client, &stmt, params).await
}
pub(crate) async fn query_explain_on(
client: &deadpool_postgres::Object,
sql: &str,
params: &[DecodedValue],
) -> Result<String> {
let wrapped = format!("EXPLAIN (ANALYZE, FORMAT JSON, VERBOSE) {sql}");
let stmt = client.prepare_cached(&wrapped).await?;
let bound: Vec<BoundParam<'_>> = params.iter().map(BoundParam).collect();
let param_refs: Vec<&(dyn postgres_types::ToSql + Sync)> =
bound.iter().map(|p| p as &(dyn postgres_types::ToSql + Sync)).collect();
let rows = client.query(&stmt, ¶m_refs).await?;
let row = rows
.into_iter()
.next()
.ok_or_else(|| Error::message("EXPLAIN produced no output row".to_string()))?;
let RawBytes(bytes) = row.try_get::<_, RawBytes<'_>>(0)?;
Ok(String::from_utf8_lossy(bytes).into_owned())
}
#[derive(Debug)]
pub struct PgTransaction {
client: deadpool_postgres::Object,
types: ExtensionOids,
}
impl PgTransaction {
pub fn types(&self) -> &ExtensionOids {
&self.types
}
pub async fn query_typed_with_globals(
&self,
sql: &str,
params: &[DecodedValue],
ext: &ExtensionOids,
globals: &str,
) -> Result<Vec<DecodedValue>> {
set_globals_on(&self.client, globals, true).await?;
query_typed_on(&self.client, sql, params, ext).await
}
pub async fn execute_typed_with_globals(&self, sql: &str, params: &[DecodedValue], globals: &str) -> Result<u64> {
set_globals_on(&self.client, globals, true).await?;
execute_typed_on(&self.client, sql, params).await
}
pub async fn query_typed(
&self,
sql: &str,
params: &[DecodedValue],
ext: &ExtensionOids,
) -> Result<Vec<DecodedValue>> {
query_typed_on(&self.client, sql, params, ext).await
}
pub async fn execute_typed(&self, sql: &str, params: &[DecodedValue]) -> Result<u64> {
execute_typed_on(&self.client, sql, params).await
}
pub async fn batch_execute(&self, sql: &str) -> Result<()> {
self.client.batch_execute(sql).await?;
Ok(())
}
pub async fn savepoint(&self, name: &str) -> Result<()> {
self.client
.batch_execute(&format!("SAVEPOINT {}", listener::quote_ident(name)))
.await?;
Ok(())
}
pub async fn release_savepoint(&self, name: &str) -> Result<()> {
self.client
.batch_execute(&format!("RELEASE SAVEPOINT {}", listener::quote_ident(name)))
.await?;
Ok(())
}
pub async fn rollback_to_savepoint(&self, name: &str) -> Result<()> {
self.client
.batch_execute(&format!("ROLLBACK TO SAVEPOINT {}", listener::quote_ident(name)))
.await?;
Ok(())
}
pub async fn commit(self) -> Result<()> {
match self.client.batch_execute("COMMIT").await {
Ok(()) => Ok(()),
Err(e) => {
let _ = self.client.batch_execute("ROLLBACK").await;
Err(e.into())
}
}
}
pub async fn rollback(self) -> Result<()> {
self.client.batch_execute("ROLLBACK").await?;
Ok(())
}
}
fn session_config(dsn: &str) -> Result<tokio_postgres::Config> {
let mut config: tokio_postgres::Config = dsn.parse()?;
let pinned = "-c timezone=UTC -c intervalstyle=iso_8601";
let options = match config.get_options() {
Some(existing) => format!("{existing} {pinned}"),
None => pinned.to_string(),
};
config.options(options);
Ok(config)
}
pub(crate) async fn discover_types(client: &tokio_postgres::Client) -> Result<ExtensionOids> {
let rows = client.query(wire::TYPE_DISCOVERY_SQL, &[]).await?;
Ok(ExtensionOids::from_discovery_rows(rows.iter().map(|r| {
let oid: i64 = r.get(0);
let typtype: String = r.get(1);
let base_oid: i64 = r.get(2);
let typname: String = r.get(3);
(oid as u32, typtype, base_oid as u32, typname)
})))
}
fn decode_result_column(row: &tokio_postgres::Row, ext: &ExtensionOids) -> Result<DecodedValue> {
let oid = row.columns()[0].type_().oid();
match row.try_get::<_, Option<RawBytes>>(0)? {
None => Ok(DecodedValue::Null),
Some(RawBytes(bytes)) => wire::decode_value(oid, bytes, ext),
}
}
fn decode_row_named(row: &tokio_postgres::Row, ext: &ExtensionOids) -> Result<DecodedValue> {
let mut fields = Vec::with_capacity(row.columns().len());
for (i, col) in row.columns().iter().enumerate() {
let oid = col.type_().oid();
let value = match row.try_get::<_, Option<RawBytes>>(i)? {
None => DecodedValue::Null,
Some(RawBytes(bytes)) => wire::decode_value(oid, bytes, ext)?,
};
fields.push((col.name().to_string(), value));
}
Ok(DecodedValue::Object(fields))
}
#[cfg(test)]
mod tests {
use super::*;
fn test_dsn() -> String {
std::env::var("PYLON_PGCON_TEST_DSN").expect("PYLON_PGCON_TEST_DSN must be set to run live-Postgres tests")
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn connects_and_round_trips_a_scalar_query() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
let rows = pool.query_raw("SELECT 1 + 1").await.unwrap();
assert_eq!(rows.len(), 1);
let value: i32 = rows[0].get(0);
assert_eq!(value, 2);
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn pool_is_reused_across_multiple_queries() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
for i in 0..5 {
let rows = pool.query_raw(&format!("SELECT {i}")).await.unwrap();
let value: i32 = rows[0].get(0);
assert_eq!(value, i);
}
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn invalid_dsn_fails_to_connect() {
let result = PgPool::connect("not-a-valid-dsn", 5).await;
assert!(result.is_err());
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn connect_fails_eagerly_against_a_nonexistent_database() {
let dsn = test_dsn();
let (prefix, _db) = dsn.rsplit_once('/').expect("DSN must have a database path segment");
let bad_dsn = format!("{prefix}/pgcon_definitely_does_not_exist");
let result = PgPool::connect(&bad_dsn, 5).await;
assert!(result.is_err());
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn bad_sql_returns_an_error_not_a_panic() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
let result = pool.query_raw("SELECT this is not valid sql").await;
assert!(result.is_err());
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn decodes_a_bare_scalar_result_column() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
let rows = pool
.query_composite("SELECT 42::int8 AS result", &ExtensionOids::default())
.await
.unwrap();
assert_eq!(rows, vec![DecodedValue::I64(42)]);
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn decodes_a_composite_matching_pylon_cores_own_emission_shape() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
let sql = "SELECT ('Person'::text, 'Alice'::text, 30::int8, NULL::text) AS result";
let rows = pool.query_composite(sql, &ExtensionOids::default()).await.unwrap();
assert_eq!(
rows,
vec![DecodedValue::Composite(vec![
DecodedValue::Str("Person".into()),
DecodedValue::Str("Alice".into()),
DecodedValue::I64(30),
DecodedValue::Null,
])]
);
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn decodes_nested_composite_and_array_of_composite_for_real() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
let sql = "SELECT (\
'Product'::text, \
ROW('Tag'::text, 'sale'::text), \
ARRAY[ROW(1::int8), ROW(2::int8)]::record[]\
) AS result";
let rows = pool.query_composite(sql, &ExtensionOids::default()).await.unwrap();
assert_eq!(
rows,
vec![DecodedValue::Composite(vec![
DecodedValue::Str("Product".into()),
DecodedValue::Composite(vec![DecodedValue::Str("Tag".into()), DecodedValue::Str("sale".into())]),
DecodedValue::Array(vec![
DecodedValue::Composite(vec![DecodedValue::I64(1)]),
DecodedValue::Composite(vec![DecodedValue::I64(2)]),
]),
])]
);
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn decodes_array_of_text_for_real() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
let sql = "SELECT (ARRAY['a', 'b', NULL]::text[]) AS result";
let rows = pool.query_composite(sql, &ExtensionOids::default()).await.unwrap();
assert_eq!(
rows,
vec![DecodedValue::Array(vec![
DecodedValue::Str("a".into()),
DecodedValue::Str("b".into()),
DecodedValue::Null,
])]
);
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn decodes_numeric_and_jsonb_and_uuid_for_real() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
let sql = "SELECT (\
12.50::numeric, \
'{\"a\": 1, \"b\": [1,2]}'::jsonb, \
'11111111-1111-1111-1111-111111111111'::uuid\
) AS result";
let rows = pool.query_composite(sql, &ExtensionOids::default()).await.unwrap();
let DecodedValue::Composite(fields) = &rows[0] else {
panic!("expected Composite")
};
assert_eq!(fields[0], DecodedValue::Decimal("12.50".to_string()));
assert_eq!(
fields[1],
DecodedValue::Object(vec![
("a".into(), DecodedValue::I64(1)),
(
"b".into(),
DecodedValue::Array(vec![DecodedValue::I64(1), DecodedValue::I64(2)])
),
])
);
assert_eq!(fields[2], DecodedValue::Uuid([0x11; 16]));
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn decodes_bytea_for_real() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
let rows = pool
.query_composite("SELECT '\\xdeadbeef'::bytea AS result", &ExtensionOids::default())
.await
.unwrap();
assert_eq!(rows, vec![DecodedValue::Bytes(vec![0xde, 0xad, 0xbe, 0xef])]);
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn decodes_enum_cast_to_text_for_real() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
let sql = "DO $$ BEGIN CREATE TYPE pgcon_test_enum AS ENUM ('a', 'b'); \
EXCEPTION WHEN duplicate_object THEN NULL; END $$;";
pool.query_raw(sql).await.ok();
let rows = pool
.query_composite(
"SELECT ('a'::pgcon_test_enum::text) AS result",
&ExtensionOids::default(),
)
.await
.unwrap();
assert_eq!(rows, vec![DecodedValue::Str("a".to_string())]);
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn decodes_a_real_pgvector_value_through_connect_time_discovery() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
if pool.query_raw("CREATE EXTENSION IF NOT EXISTS vector").await.is_err() {
eprintln!("skipping: pgvector not installable on this server");
return;
}
let mut pool = pool;
pool.refresh_types().await.unwrap();
assert!(
pool.types().vector.is_some(),
"discovery should have found the vector OID"
);
let rows = pool
.query_composite("SELECT ('doc', '[1.5,2.5]'::vector) AS result", pool.types())
.await
.unwrap();
let DecodedValue::Composite(fields) = &rows[0] else {
panic!("expected Composite, got {:?}", rows[0])
};
assert_eq!(fields[0], DecodedValue::Str("doc".to_string()));
assert_eq!(
fields[1],
DecodedValue::Array(vec![DecodedValue::F64(1.5), DecodedValue::F64(2.5)]),
"a vector must decode to its floats, not to mojibake or an error"
);
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn decodes_a_real_enum_and_domain_through_connect_time_discovery() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
pool.query_raw(
"DO $$ BEGIN CREATE TYPE pgcon_disc_enum AS ENUM ('x', 'y'); \
EXCEPTION WHEN duplicate_object THEN NULL; END $$;",
)
.await
.ok();
pool.query_raw(
"DO $$ BEGIN CREATE DOMAIN pgcon_disc_domain AS int8 CHECK (VALUE > 0); \
EXCEPTION WHEN duplicate_object THEN NULL; END $$;",
)
.await
.ok();
let mut pool = pool;
pool.refresh_types().await.unwrap();
let rows = pool
.query_composite(
"SELECT ('x'::pgcon_disc_enum, 42::pgcon_disc_domain) AS result",
pool.types(),
)
.await
.unwrap();
let DecodedValue::Composite(fields) = &rows[0] else {
panic!("expected Composite, got {:?}", rows[0])
};
assert_eq!(fields[0], DecodedValue::Str("x".to_string()));
assert_eq!(
fields[1],
DecodedValue::I64(42),
"a domain must decode as its base type, not as text"
);
}
async fn round_trip(pool: &PgPool, pg_type: &str, param: DecodedValue) -> DecodedValue {
let sql = format!("SELECT ($1::{pg_type}) AS result");
let rows = pool
.query_typed(&sql, &[param], &ExtensionOids::default())
.await
.unwrap();
rows.into_iter().next().unwrap()
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn query_explain_returns_parseable_json_with_a_plan_node() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
let raw = pool.query_explain("SELECT 1 + 1", &[]).await.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&raw).unwrap();
assert!(parsed[0]["Plan"]["Node Type"].is_string());
assert!(parsed[0]["Plan"]["Actual Total Time"].is_number());
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn query_explain_binds_params_the_same_way_query_typed_does() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
let raw = pool
.query_explain("SELECT $1::int8 + 1", &[DecodedValue::I64(41)])
.await
.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&raw).unwrap();
assert!(parsed[0]["Plan"]["Node Type"].is_string());
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn round_trips_bool_param() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
assert_eq!(
round_trip(&pool, "bool", DecodedValue::Bool(true)).await,
DecodedValue::Bool(true)
);
assert_eq!(
round_trip(&pool, "bool", DecodedValue::Bool(false)).await,
DecodedValue::Bool(false)
);
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn round_trips_integer_params_at_every_width() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
assert_eq!(
round_trip(&pool, "int2", DecodedValue::I64(30)).await,
DecodedValue::I64(30)
);
assert_eq!(
round_trip(&pool, "int4", DecodedValue::I64(70_000)).await,
DecodedValue::I64(70_000)
);
assert_eq!(
round_trip(&pool, "int8", DecodedValue::I64(9_223_372_036_854_775_807)).await,
DecodedValue::I64(9_223_372_036_854_775_807)
);
assert_eq!(
round_trip(&pool, "int8", DecodedValue::I64(-1)).await,
DecodedValue::I64(-1)
);
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn round_trips_float_params() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
assert_eq!(
round_trip(&pool, "float4", DecodedValue::F64(1.5)).await,
DecodedValue::F64(1.5)
);
assert_eq!(
round_trip(&pool, "float8", DecodedValue::F64(2.25)).await,
DecodedValue::F64(2.25)
);
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn round_trips_text_param() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
assert_eq!(
round_trip(&pool, "text", DecodedValue::Str("héllo 🎉".to_string())).await,
DecodedValue::Str("héllo 🎉".to_string())
);
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn round_trips_bytea_param() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
assert_eq!(
round_trip(&pool, "bytea", DecodedValue::Bytes(vec![1, 2, 3, 255])).await,
DecodedValue::Bytes(vec![1, 2, 3, 255])
);
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn round_trips_uuid_param() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
let bytes = [0x11u8; 16];
assert_eq!(
round_trip(&pool, "uuid", DecodedValue::Uuid(bytes)).await,
DecodedValue::Uuid(bytes)
);
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn binds_a_plain_string_as_a_uuid_param() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
let as_string = DecodedValue::Str("11111111-1111-1111-1111-111111111111".to_string());
assert_eq!(
round_trip(&pool, "uuid", as_string).await,
DecodedValue::Uuid([0x11; 16])
);
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn binds_a_plain_string_as_a_jsonb_param() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
let as_string = DecodedValue::Str(r#"{"a":1,"b":[1,2]}"#.to_string());
assert_eq!(
round_trip(&pool, "jsonb", as_string).await,
DecodedValue::Object(vec![
("a".into(), DecodedValue::I64(1)),
(
"b".into(),
DecodedValue::Array(vec![DecodedValue::I64(1), DecodedValue::I64(2)])
),
])
);
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn round_trips_numeric_param() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
assert_eq!(
round_trip(&pool, "numeric", DecodedValue::Decimal("12.50".to_string())).await,
DecodedValue::Decimal("12.50".to_string())
);
assert_eq!(
round_trip(&pool, "numeric", DecodedValue::Decimal("-9999.001".to_string())).await,
DecodedValue::Decimal("-9999.001".to_string())
);
for text in [
"0.00000039999999999999998189924473035450347424557548947632312774658203125",
"0.00000012222222222222222222222222222222222222222",
"12222222222222222222222222222222000000000",
"100000000000000000000000000000",
"123456789012345678901234567890.123456789012345678901234567890",
] {
assert_eq!(
round_trip(&pool, "numeric", DecodedValue::Decimal(text.to_string())).await,
DecodedValue::Decimal(text.to_string()),
"{text}"
);
}
assert_eq!(
round_trip(&pool, "numeric", DecodedValue::Str("0.0000004".to_string())).await,
DecodedValue::Decimal("0.0000004".to_string())
);
assert_eq!(
round_trip(&pool, "numeric", DecodedValue::I64(-12345)).await,
DecodedValue::Decimal("-12345".to_string())
);
assert_eq!(
round_trip(&pool, "numeric", DecodedValue::F64(0.1)).await,
DecodedValue::Decimal("0.1".to_string())
);
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn round_trips_null_param() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
assert_eq!(round_trip(&pool, "int8", DecodedValue::Null).await, DecodedValue::Null);
assert_eq!(round_trip(&pool, "text", DecodedValue::Null).await, DecodedValue::Null);
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn round_trips_array_param() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
let param = DecodedValue::Array(vec![
DecodedValue::Str("a".into()),
DecodedValue::Str("b".into()),
DecodedValue::Null,
]);
assert_eq!(round_trip(&pool, "text[]", param.clone()).await, param);
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn round_trips_int_array_param() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
let param = DecodedValue::Array(vec![DecodedValue::I64(1), DecodedValue::I64(2), DecodedValue::I64(3)]);
assert_eq!(round_trip(&pool, "int8[]", param.clone()).await, param);
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn round_trips_jsonb_object_param() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
let param = DecodedValue::Object(vec![
("a".into(), DecodedValue::I64(1)),
("b".into(), DecodedValue::Str("two".into())),
(
"c".into(),
DecodedValue::Array(vec![DecodedValue::I64(1), DecodedValue::I64(2)]),
),
]);
assert_eq!(round_trip(&pool, "jsonb", param.clone()).await, param);
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn query_typed_matches_pylon_cores_own_param_binding_convention() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
let sql = "SELECT ($1::text, $2::int8, $3::bool) AS result";
let params = vec![
DecodedValue::Str("Alice".into()),
DecodedValue::I64(30),
DecodedValue::Bool(true),
];
let rows = pool.query_typed(sql, ¶ms, &ExtensionOids::default()).await.unwrap();
assert_eq!(
rows,
vec![DecodedValue::Composite(vec![
DecodedValue::Str("Alice".into()),
DecodedValue::I64(30),
DecodedValue::Bool(true),
])]
);
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn wrong_param_count_returns_an_error_not_a_panic() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
let result = pool
.query_typed(
"SELECT $1::int8, $2::int8",
&[DecodedValue::I64(1)],
&ExtensionOids::default(),
)
.await;
assert!(result.is_err());
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn execute_typed_runs_a_mutation_and_reports_affected_rows() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
pool.query_raw("CREATE TEMP TABLE IF NOT EXISTS pgcon_execute_test (id int8, name text)")
.await
.unwrap();
let inserted = pool
.execute_typed(
"INSERT INTO pgcon_execute_test (id, name) VALUES ($1::int8, $2::text)",
&[DecodedValue::I64(1), DecodedValue::Str("alice".into())],
)
.await
.unwrap();
assert_eq!(inserted, 1);
let updated = pool
.execute_typed(
"UPDATE pgcon_execute_test SET name = $1::text WHERE id = $2::int8",
&[DecodedValue::Str("bob".into()), DecodedValue::I64(1)],
)
.await
.unwrap();
assert_eq!(updated, 1);
let rows = pool
.query_composite(
"SELECT (name) AS result FROM pgcon_execute_test",
&ExtensionOids::default(),
)
.await
.unwrap();
assert_eq!(rows, vec![DecodedValue::Str("bob".to_string())]);
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn unique_violation_reports_23505() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
pool.query_raw("CREATE TEMP TABLE pgcon_unique_test (id int8 PRIMARY KEY)")
.await
.unwrap();
pool.execute_typed(
"INSERT INTO pgcon_unique_test (id) VALUES ($1::int8)",
&[DecodedValue::I64(1)],
)
.await
.unwrap();
let err = pool
.execute_typed(
"INSERT INTO pgcon_unique_test (id) VALUES ($1::int8)",
&[DecodedValue::I64(1)],
)
.await
.unwrap_err();
assert_eq!(err.sqlstate(), Some(&tokio_postgres::error::SqlState::UNIQUE_VIOLATION));
assert_eq!(err.sqlstate().unwrap().code(), "23505");
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn foreign_key_violation_reports_23503() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
pool.query_raw("CREATE TEMP TABLE pgcon_fk_parent (id int8 PRIMARY KEY)")
.await
.unwrap();
pool.query_raw("CREATE TEMP TABLE pgcon_fk_child (parent_id int8 REFERENCES pgcon_fk_parent(id))")
.await
.unwrap();
let err = pool
.execute_typed(
"INSERT INTO pgcon_fk_child (parent_id) VALUES ($1::int8)",
&[DecodedValue::I64(999)],
)
.await
.unwrap_err();
assert_eq!(
err.sqlstate(),
Some(&tokio_postgres::error::SqlState::FOREIGN_KEY_VIOLATION)
);
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn check_violation_reports_23514() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
pool.query_raw("CREATE TEMP TABLE pgcon_check_test (age int8 CHECK (age >= 0))")
.await
.unwrap();
let err = pool
.execute_typed(
"INSERT INTO pgcon_check_test (age) VALUES ($1::int8)",
&[DecodedValue::I64(-1)],
)
.await
.unwrap_err();
assert_eq!(err.sqlstate(), Some(&tokio_postgres::error::SqlState::CHECK_VIOLATION));
assert_eq!(err.violated_table().map(|(_, table)| table), Some("pgcon_check_test"));
assert_eq!(err.violated_scalar(), None);
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn domain_check_violation_reports_the_domain_name_not_the_constraint_name() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
pool.query_raw(
"DO $$ BEGIN CREATE DOMAIN pgcon_rating AS int8 CHECK (VALUE BETWEEN 1 AND 5); \
EXCEPTION WHEN duplicate_object THEN NULL; END $$",
)
.await
.unwrap();
pool.query_raw("CREATE TEMP TABLE pgcon_domain_check_test (rating pgcon_rating)")
.await
.unwrap();
let err = pool
.execute_typed(
"INSERT INTO pgcon_domain_check_test (rating) VALUES ($1::pgcon_rating)",
&[DecodedValue::I64(99)],
)
.await
.unwrap_err();
assert_eq!(err.sqlstate(), Some(&tokio_postgres::error::SqlState::CHECK_VIOLATION));
assert_eq!(err.violated_scalar(), Some(("public", "pgcon_rating")));
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn syntax_error_has_no_sqlstate_matching_constraint_codes() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
let err = pool.query_raw("SELECT this is not valid sql").await.unwrap_err();
assert_ne!(err.sqlstate(), Some(&tokio_postgres::error::SqlState::UNIQUE_VIOLATION));
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn connection_pool_error_has_no_sqlstate() {
let result = PgPool::connect("not-a-valid-dsn", 5).await;
let err = result.unwrap_err();
assert_eq!(err.sqlstate(), None);
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn committed_transaction_persists_its_writes() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
pool.query_raw("CREATE TEMP TABLE pgcon_tx_commit_test (id int8 PRIMARY KEY)")
.await
.unwrap();
let tx = pool.begin("serializable").await.unwrap();
tx.execute_typed(
"INSERT INTO pgcon_tx_commit_test (id) VALUES ($1::int8)",
&[DecodedValue::I64(1)],
)
.await
.unwrap();
tx.commit().await.unwrap();
let rows = pool
.query_composite(
"SELECT (id) AS result FROM pgcon_tx_commit_test",
&ExtensionOids::default(),
)
.await
.unwrap();
assert_eq!(rows, vec![DecodedValue::I64(1)]);
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn rolled_back_transaction_discards_its_writes() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
pool.query_raw("CREATE TEMP TABLE pgcon_tx_rollback_test (id int8 PRIMARY KEY)")
.await
.unwrap();
let tx = pool.begin("serializable").await.unwrap();
tx.execute_typed(
"INSERT INTO pgcon_tx_rollback_test (id) VALUES ($1::int8)",
&[DecodedValue::I64(1)],
)
.await
.unwrap();
tx.rollback().await.unwrap();
let rows = pool
.query_composite(
"SELECT (id) AS result FROM pgcon_tx_rollback_test",
&ExtensionOids::default(),
)
.await
.unwrap();
assert!(rows.is_empty());
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn begin_actually_sets_the_requested_isolation_level() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
for (level, expected) in [
("read_committed", "read committed"),
("repeatable_read", "repeatable read"),
("serializable", "serializable"),
] {
let tx = pool.begin(level).await.unwrap();
let rows = tx
.query_typed(
"SELECT (current_setting('transaction_isolation')) AS result",
&[],
&ExtensionOids::default(),
)
.await
.unwrap();
assert_eq!(rows, vec![DecodedValue::Str(expected.to_string())]);
tx.rollback().await.unwrap();
}
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn begin_rejects_an_unknown_isolation_level() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
let result = pool.begin("not_a_real_level").await;
assert!(result.is_err());
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn a_pooled_connection_is_reusable_after_commit_and_after_rollback() {
let pool = PgPool::connect(&test_dsn(), 1).await.unwrap();
let tx = pool.begin("serializable").await.unwrap();
tx.commit().await.unwrap();
let tx = pool.begin("serializable").await.unwrap();
tx.rollback().await.unwrap();
let rows = pool.query_raw("SELECT 1").await.unwrap();
assert_eq!(rows.len(), 1);
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn failed_commit_leaves_the_connection_reusable() {
let pool = PgPool::connect(&test_dsn(), 2).await.unwrap();
pool.query_raw("DROP TABLE IF EXISTS pgcon_tx_failed_commit_test")
.await
.unwrap();
pool.query_raw("CREATE TABLE pgcon_tx_failed_commit_test (class int8, value int8)")
.await
.unwrap();
pool.execute_typed(
"INSERT INTO pgcon_tx_failed_commit_test (class, value) VALUES ($1::int8, $2::int8), ($3::int8, $4::int8)",
&[
DecodedValue::I64(1),
DecodedValue::I64(10),
DecodedValue::I64(2),
DecodedValue::I64(20),
],
)
.await
.unwrap();
let tx1 = pool.begin("serializable").await.unwrap();
let tx2 = pool.begin("serializable").await.unwrap();
tx1.query_typed(
"SELECT (sum(value)) AS result FROM pgcon_tx_failed_commit_test WHERE class = 1::int8",
&[],
&ExtensionOids::default(),
)
.await
.unwrap();
tx2.query_typed(
"SELECT (sum(value)) AS result FROM pgcon_tx_failed_commit_test WHERE class = 2::int8",
&[],
&ExtensionOids::default(),
)
.await
.unwrap();
tx1.execute_typed(
"INSERT INTO pgcon_tx_failed_commit_test (class, value) VALUES (2::int8, $1::int8)",
&[DecodedValue::I64(10)],
)
.await
.unwrap();
tx2.execute_typed(
"INSERT INTO pgcon_tx_failed_commit_test (class, value) VALUES (1::int8, $1::int8)",
&[DecodedValue::I64(20)],
)
.await
.unwrap();
tx1.commit().await.unwrap();
let commit_result = tx2.commit().await;
assert!(commit_result.is_err());
for _ in 0..2 {
let rows = pool.query_raw("SELECT 1").await.unwrap();
assert_eq!(rows.len(), 1);
}
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn serializable_transactions_conflict_with_40001() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
pool.query_raw("DROP TABLE IF EXISTS pgcon_serialization_test")
.await
.unwrap();
pool.query_raw("CREATE TABLE pgcon_serialization_test (class int8, value int8)")
.await
.unwrap();
pool.execute_typed(
"INSERT INTO pgcon_serialization_test (class, value) VALUES ($1::int8, $2::int8), ($3::int8, $4::int8)",
&[
DecodedValue::I64(1),
DecodedValue::I64(10),
DecodedValue::I64(2),
DecodedValue::I64(20),
],
)
.await
.unwrap();
let tx1 = pool.begin("serializable").await.unwrap();
let tx2 = pool.begin("serializable").await.unwrap();
tx1.query_typed(
"SELECT (sum(value)) AS result FROM pgcon_serialization_test WHERE class = 1::int8",
&[],
&ExtensionOids::default(),
)
.await
.unwrap();
tx2.query_typed(
"SELECT (sum(value)) AS result FROM pgcon_serialization_test WHERE class = 2::int8",
&[],
&ExtensionOids::default(),
)
.await
.unwrap();
tx1.execute_typed(
"INSERT INTO pgcon_serialization_test (class, value) VALUES (2::int8, $1::int8)",
&[DecodedValue::I64(10)],
)
.await
.unwrap();
tx2.execute_typed(
"INSERT INTO pgcon_serialization_test (class, value) VALUES (1::int8, $1::int8)",
&[DecodedValue::I64(20)],
)
.await
.unwrap();
tx1.commit().await.unwrap();
let err = tx2.commit().await.unwrap_err();
assert_eq!(
err.sqlstate(),
Some(&tokio_postgres::error::SqlState::T_R_SERIALIZATION_FAILURE)
);
}
#[tokio::test]
#[ignore = "requires a live Postgres via PYLON_PGCON_TEST_DSN"]
async fn concurrent_transactions_deadlock_with_40p01() {
let pool = PgPool::connect(&test_dsn(), 5).await.unwrap();
pool.query_raw("DROP TABLE IF EXISTS pgcon_deadlock_test")
.await
.unwrap();
pool.query_raw("CREATE TABLE pgcon_deadlock_test (id int8 PRIMARY KEY, value int8)")
.await
.unwrap();
pool.execute_typed(
"INSERT INTO pgcon_deadlock_test (id, value) VALUES ($1::int8, $2::int8), ($3::int8, $4::int8)",
&[
DecodedValue::I64(1),
DecodedValue::I64(0),
DecodedValue::I64(2),
DecodedValue::I64(0),
],
)
.await
.unwrap();
let tx1 = pool.begin("read_committed").await.unwrap();
let tx2 = pool.begin("read_committed").await.unwrap();
tx1.execute_typed("UPDATE pgcon_deadlock_test SET value = 1::int8 WHERE id = 1::int8", &[])
.await
.unwrap();
tx2.execute_typed("UPDATE pgcon_deadlock_test SET value = 2::int8 WHERE id = 2::int8", &[])
.await
.unwrap();
let (r1, r2) = tokio::join!(
tx1.execute_typed("UPDATE pgcon_deadlock_test SET value = 3::int8 WHERE id = 2::int8", &[]),
tx2.execute_typed("UPDATE pgcon_deadlock_test SET value = 4::int8 WHERE id = 1::int8", &[]),
);
let results = [r1, r2];
let deadlock_errors: Vec<_> = results
.iter()
.filter(|r| matches!(r, Err(e) if e.sqlstate() == Some(&tokio_postgres::error::SqlState::T_R_DEADLOCK_DETECTED)))
.collect();
assert_eq!(
deadlock_errors.len(),
1,
"expected exactly one side to be aborted with 40P01, got {results:?}"
);
}
}
#[cfg(test)]
mod error_message_tests {
use super::*;
#[tokio::test]
#[ignore = "requires a live database"]
async fn a_server_error_renders_the_server_message() {
let pool = PgPool::connect(&std::env::var("PYLON_PGCON_TEST_DSN").unwrap(), 2)
.await
.unwrap();
let err = pool
.query_typed("SELECT 1 FROM pylon_no_such_table", &[], pool.types())
.await
.unwrap_err();
let rendered = err.to_string();
assert!(
rendered.contains("pylon_no_such_table"),
"expected the server's own message, got {rendered:?}"
);
assert_ne!(rendered, "db error");
}
}