From 5aa8c2fd5e667044061a78873e67c9cfee7a21f5 Mon Sep 17 00:00:00 2001 From: Joey de Waal Date: Mon, 16 Dec 2024 21:55:21 +0100 Subject: [PATCH 1/3] feat(Postgres): support nested domain types --- sqlx-postgres/src/type_info.rs | 13 ++++++++- sqlx-postgres/src/types/record.rs | 47 +++++++++++++++++-------------- tests/postgres/setup.sql | 13 +++++++++ tests/postgres/types.rs | 30 ++++++++++++++++++++ 4 files changed, 81 insertions(+), 22 deletions(-) diff --git a/sqlx-postgres/src/type_info.rs b/sqlx-postgres/src/type_info.rs index 28c56758e9..34c17a740e 100644 --- a/sqlx-postgres/src/type_info.rs +++ b/sqlx-postgres/src/type_info.rs @@ -1013,7 +1013,7 @@ impl PgType { /// If `soft_eq` is true and `self` or `other` is `DeclareWithOid` but not both, return `true` /// before checking names. fn eq_impl(&self, other: &Self, soft_eq: bool) -> bool { - if let (Some(a), Some(b)) = (self.try_oid(), other.try_oid()) { + if let (Some(a), Some(b)) = (self.base_oid(), other.base_oid()) { // If there are OIDs available, use OIDs to perform a direct match return a == b; } @@ -1035,6 +1035,17 @@ impl PgType { // Otherwise, perform a match on the name name_eq(self.name(), other.name()) } + + // Returns the OID of the type, returns the OID of the base_type for Domain types + fn base_oid(&self) -> Option { + match self { + PgType::Custom(custom) => match &custom.kind { + PgTypeKind::Domain(domain) => domain.base_oid(), + _ => Some(custom.oid), + }, + ty => ty.try_oid(), + } + } } impl TypeInfo for PgTypeInfo { diff --git a/sqlx-postgres/src/types/record.rs b/sqlx-postgres/src/types/record.rs index 6e37182c40..797ae649aa 100644 --- a/sqlx-postgres/src/types/record.rs +++ b/sqlx-postgres/src/types/record.rs @@ -103,27 +103,7 @@ impl<'r> PgRecordDecoder<'r> { match self.fmt { PgValueFormat::Binary => { let element_type_oid = Oid(self.buf.get_u32()); - let element_type_opt = match self.typ.0.kind() { - PgTypeKind::Simple if self.typ.0 == PgType::Record => { - PgTypeInfo::try_from_oid(element_type_oid) - } - - PgTypeKind::Composite(fields) => { - let ty = fields[self.ind].1.clone(); - if ty.0.oid() != element_type_oid { - return Err("unexpected mismatch of composite type information".into()); - } - - Some(ty) - } - - _ => { - return Err( - "unexpected non-composite type being decoded as a composite type" - .into(), - ); - } - }; + let element_type_opt = self.find_type_info(&self.typ, element_type_oid)?; if let Some(ty) = &element_type_opt { if !ty.is_null() && !T::compatible(ty) { @@ -202,4 +182,29 @@ impl<'r> PgRecordDecoder<'r> { } } } + fn find_type_info( + &self, + typ: &PgTypeInfo, + oid: Oid, + ) -> Result, BoxDynError> { + match typ.kind() { + PgTypeKind::Simple if typ.0 == PgType::Record => Ok(PgTypeInfo::try_from_oid(oid)), + + PgTypeKind::Composite(fields) => { + let ty = fields[self.ind].1.clone(); + if ty.0.oid() != oid { + return Err("unexpected mismatch of composite type information".into()); + } + + Ok(Some(ty)) + } + PgTypeKind::Domain(d) => self.find_type_info(d, oid), + + _ => { + return Err( + "unexpected non-composite type being decoded as a composite type".into(), + ); + } + } + } } diff --git a/tests/postgres/setup.sql b/tests/postgres/setup.sql index e3b35d3158..f87ea5cc85 100644 --- a/tests/postgres/setup.sql +++ b/tests/postgres/setup.sql @@ -63,3 +63,16 @@ CREATE SCHEMA IF NOT EXISTS foo; CREATE TYPE foo."Foo" as ENUM ('Bar', 'Baz'); CREATE TABLE mytable(f HSTORE); + +CREATE DOMAIN positive_int AS integer CHECK (VALUE >= 0); +CREATE DOMAIN percentage AS positive_int CHECK (VALUE < 100); + +CREATE TYPE person as ( + id int, + age positive_int, + percent percentage +); + +CREATE TYPE leaf_composite AS (prim integer); +CREATE DOMAIN domain AS leaf_composite; +CREATE TYPE root_composite AS (domain domain); diff --git a/tests/postgres/types.rs b/tests/postgres/types.rs index 0d15caf8de..3013190390 100644 --- a/tests/postgres/types.rs +++ b/tests/postgres/types.rs @@ -635,6 +635,36 @@ test_type!(ltree_vec>(Postgres, sqlx::postgres::types::PgLTree::try_from_iter(["Alpha", "Beta", "Delta", "Gamma"]).unwrap() ] )); +#[derive(sqlx::Type, Debug, PartialEq)] +struct Person { + id: i32, + age: i32, + percent: i32, +} + +test_type!(nested_domain_types(Postgres, + "ROW(1, 21::positive_int, 50::percentage)::person" == Person { id: 1, age: 21, percent: 50 }) +); + +#[derive(sqlx::Type, Debug, PartialEq)] +#[sqlx(type_name = "leaf_composite")] +struct LeafComposite { + prim: i32, +} + +#[derive(sqlx::Type, Debug, PartialEq)] +#[sqlx(type_name = "domain")] +struct Domain(LeafComposite); + +#[derive(sqlx::Type, Debug, PartialEq)] +#[sqlx(type_name = "root_composite")] +struct RootComposite { + domain: Domain, +} + +test_type!(nested_domain_types_2(Postgres, + "ROW(ROW(1))::root_composite" == RootComposite { domain: Domain(LeafComposite { prim: 1})}) +); #[sqlx_macros::test] async fn test_text_adapter() -> anyhow::Result<()> { From 4b1e96eaed98ef58f868385ea1ea7966d263cbf9 Mon Sep 17 00:00:00 2001 From: Joey de Waal Date: Wed, 5 Mar 2025 09:35:50 +0100 Subject: [PATCH 2/3] chore: clippy --- sqlx-postgres/src/types/record.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/sqlx-postgres/src/types/record.rs b/sqlx-postgres/src/types/record.rs index 797ae649aa..656f898eb5 100644 --- a/sqlx-postgres/src/types/record.rs +++ b/sqlx-postgres/src/types/record.rs @@ -200,11 +200,7 @@ impl<'r> PgRecordDecoder<'r> { } PgTypeKind::Domain(d) => self.find_type_info(d, oid), - _ => { - return Err( - "unexpected non-composite type being decoded as a composite type".into(), - ); - } + _ => Err("unexpected non-composite type being decoded as a composite type".into()), } } } From 8948a01f904196cba11b927793ceb53cc5eef574 Mon Sep 17 00:00:00 2001 From: Joey de Waal Date: Sun, 13 Apr 2025 13:43:49 +0200 Subject: [PATCH 3/3] fix(postgres): Recurse when looking for type info. --- sqlx-postgres/src/type_info.rs | 9 +++++---- sqlx-postgres/src/types/record.rs | 7 +++---- tests/postgres/setup.sql | 2 +- tests/postgres/types.rs | 17 +++++++++++++---- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/sqlx-postgres/src/type_info.rs b/sqlx-postgres/src/type_info.rs index 34c17a740e..50504c1174 100644 --- a/sqlx-postgres/src/type_info.rs +++ b/sqlx-postgres/src/type_info.rs @@ -1013,7 +1013,7 @@ impl PgType { /// If `soft_eq` is true and `self` or `other` is `DeclareWithOid` but not both, return `true` /// before checking names. fn eq_impl(&self, other: &Self, soft_eq: bool) -> bool { - if let (Some(a), Some(b)) = (self.base_oid(), other.base_oid()) { + if let (Some(a), Some(b)) = (self.try_base_oid(), other.try_base_oid()) { // If there are OIDs available, use OIDs to perform a direct match return a == b; } @@ -1036,11 +1036,12 @@ impl PgType { name_eq(self.name(), other.name()) } - // Returns the OID of the type, returns the OID of the base_type for Domain types - fn base_oid(&self) -> Option { + // Tries to return the OID of the type, returns the OID of the base_type for domain types + #[inline(always)] + fn try_base_oid(&self) -> Option { match self { PgType::Custom(custom) => match &custom.kind { - PgTypeKind::Domain(domain) => domain.base_oid(), + PgTypeKind::Domain(domain) => domain.try_oid(), _ => Some(custom.oid), }, ty => ty.try_oid(), diff --git a/sqlx-postgres/src/types/record.rs b/sqlx-postgres/src/types/record.rs index 656f898eb5..a5410caadd 100644 --- a/sqlx-postgres/src/types/record.rs +++ b/sqlx-postgres/src/types/record.rs @@ -182,6 +182,7 @@ impl<'r> PgRecordDecoder<'r> { } } } + fn find_type_info( &self, typ: &PgTypeInfo, @@ -189,7 +190,6 @@ impl<'r> PgRecordDecoder<'r> { ) -> Result, BoxDynError> { match typ.kind() { PgTypeKind::Simple if typ.0 == PgType::Record => Ok(PgTypeInfo::try_from_oid(oid)), - PgTypeKind::Composite(fields) => { let ty = fields[self.ind].1.clone(); if ty.0.oid() != oid { @@ -198,9 +198,8 @@ impl<'r> PgRecordDecoder<'r> { Ok(Some(ty)) } - PgTypeKind::Domain(d) => self.find_type_info(d, oid), - - _ => Err("unexpected non-composite type being decoded as a composite type".into()), + PgTypeKind::Domain(domain) => self.find_type_info(domain, oid), + _ => Err("unexpected custom type being decoded as a composite type".into()), } } } diff --git a/tests/postgres/setup.sql b/tests/postgres/setup.sql index f87ea5cc85..7f2d35be01 100644 --- a/tests/postgres/setup.sql +++ b/tests/postgres/setup.sql @@ -65,7 +65,7 @@ CREATE TYPE foo."Foo" as ENUM ('Bar', 'Baz'); CREATE TABLE mytable(f HSTORE); CREATE DOMAIN positive_int AS integer CHECK (VALUE >= 0); -CREATE DOMAIN percentage AS positive_int CHECK (VALUE < 100); +CREATE DOMAIN percentage AS positive_int CHECK (VALUE <= 100); CREATE TYPE person as ( id int, diff --git a/tests/postgres/types.rs b/tests/postgres/types.rs index 3013190390..7edd1c5e91 100644 --- a/tests/postgres/types.rs +++ b/tests/postgres/types.rs @@ -635,15 +635,24 @@ test_type!(ltree_vec>(Postgres, sqlx::postgres::types::PgLTree::try_from_iter(["Alpha", "Beta", "Delta", "Gamma"]).unwrap() ] )); + +#[derive(sqlx::Type, Debug, PartialEq)] +#[sqlx(type_name = "positive_int")] +struct PositiveInt(i32); + +#[derive(sqlx::Type, Debug, PartialEq)] +#[sqlx(type_name = "percentage")] +struct Percentage(PositiveInt); + #[derive(sqlx::Type, Debug, PartialEq)] struct Person { id: i32, - age: i32, - percent: i32, + age: PositiveInt, + percent: Percentage, } -test_type!(nested_domain_types(Postgres, - "ROW(1, 21::positive_int, 50::percentage)::person" == Person { id: 1, age: 21, percent: 50 }) +test_type!(nested_domain_types_1(Postgres, + "ROW(1, 21::positive_int, 50::percentage)::person" == Person { id: 1, age: PositiveInt(21), percent: Percentage(PositiveInt(50)) }) ); #[derive(sqlx::Type, Debug, PartialEq)]