From 2f10c29dfd48dd9bac66b2fbabced6e8f0cfd445 Mon Sep 17 00:00:00 2001 From: "James H." <32926722+jayy-lmao@users.noreply.github.com> Date: Mon, 10 Mar 2025 09:01:30 +1100 Subject: [PATCH] feat(postgres): add geometry circle (#3773) * feat: circle * docs: comments --- sqlx-postgres/src/type_checking.rs | 2 + sqlx-postgres/src/types/geometry/box.rs | 5 +- sqlx-postgres/src/types/geometry/circle.rs | 250 ++++++++++++++++++ sqlx-postgres/src/types/geometry/line.rs | 5 +- .../src/types/geometry/line_segment.rs | 5 +- sqlx-postgres/src/types/geometry/mod.rs | 1 + sqlx-postgres/src/types/geometry/path.rs | 5 +- sqlx-postgres/src/types/geometry/point.rs | 5 +- sqlx-postgres/src/types/geometry/polygon.rs | 5 +- sqlx-postgres/src/types/mod.rs | 2 + tests/postgres/types.rs | 8 + 11 files changed, 287 insertions(+), 6 deletions(-) create mode 100644 sqlx-postgres/src/types/geometry/circle.rs diff --git a/sqlx-postgres/src/type_checking.rs b/sqlx-postgres/src/type_checking.rs index c82fd621..a28531c9 100644 --- a/sqlx-postgres/src/type_checking.rs +++ b/sqlx-postgres/src/type_checking.rs @@ -44,6 +44,8 @@ impl_type_checking!( sqlx::postgres::types::PgPolygon, + sqlx::postgres::types::PgCircle, + #[cfg(feature = "uuid")] sqlx::types::Uuid, diff --git a/sqlx-postgres/src/types/geometry/box.rs b/sqlx-postgres/src/types/geometry/box.rs index 988c028e..28016b27 100644 --- a/sqlx-postgres/src/types/geometry/box.rs +++ b/sqlx-postgres/src/types/geometry/box.rs @@ -23,7 +23,10 @@ const ERROR: &str = "error decoding BOX"; /// where `(upper_right_x,upper_right_y) and (lower_left_x,lower_left_y)` are any two opposite corners of the box. /// Any two opposite corners can be supplied on input, but the values will be reordered as needed to store the upper right and lower left corners, in that order. /// -/// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-GEOMETRIC-BOXES +/// See [Postgres Manual, Section 8.8.4: Geometric Types - Boxes][PG.S.8.8.4] for details. +/// +/// [PG.S.8.8.4]: https://www.postgresql.org/docs/current/datatype-geometric.html#DATATYPE-GEOMETRIC-BOXES +/// #[derive(Debug, Clone, PartialEq)] pub struct PgBox { pub upper_right_x: f64, diff --git a/sqlx-postgres/src/types/geometry/circle.rs b/sqlx-postgres/src/types/geometry/circle.rs new file mode 100644 index 00000000..dde54dd2 --- /dev/null +++ b/sqlx-postgres/src/types/geometry/circle.rs @@ -0,0 +1,250 @@ +use crate::decode::Decode; +use crate::encode::{Encode, IsNull}; +use crate::error::BoxDynError; +use crate::types::Type; +use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, Postgres}; +use sqlx_core::bytes::Buf; +use sqlx_core::Error; +use std::str::FromStr; + +const ERROR: &str = "error decoding CIRCLE"; + +/// ## Postgres Geometric Circle type +/// +/// Description: Circle +/// Representation: `< (x, y), radius >` (center point and radius) +/// +/// ```text +/// < ( x , y ) , radius > +/// ( ( x , y ) , radius ) +/// ( x , y ) , radius +/// x , y , radius +/// ``` +/// where `(x,y)` is the center point. +/// +/// See [Postgres Manual, Section 8.8.7, Geometric Types - Circles][PG.S.8.8.7] for details. +/// +/// [PG.S.8.8.7]: https://www.postgresql.org/docs/current/datatype-geometric.html#DATATYPE-CIRCLE +/// +#[derive(Debug, Clone, PartialEq)] +pub struct PgCircle { + pub x: f64, + pub y: f64, + pub radius: f64, +} + +impl Type for PgCircle { + fn type_info() -> PgTypeInfo { + PgTypeInfo::with_name("circle") + } +} + +impl PgHasArrayType for PgCircle { + fn array_type_info() -> PgTypeInfo { + PgTypeInfo::with_name("_circle") + } +} + +impl<'r> Decode<'r, Postgres> for PgCircle { + fn decode(value: PgValueRef<'r>) -> Result> { + match value.format() { + PgValueFormat::Text => Ok(PgCircle::from_str(value.as_str()?)?), + PgValueFormat::Binary => Ok(PgCircle::from_bytes(value.as_bytes()?)?), + } + } +} + +impl<'q> Encode<'q, Postgres> for PgCircle { + fn produces(&self) -> Option { + Some(PgTypeInfo::with_name("circle")) + } + + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { + self.serialize(buf)?; + Ok(IsNull::No) + } +} + +impl FromStr for PgCircle { + type Err = BoxDynError; + + fn from_str(s: &str) -> Result { + let sanitised = s.replace(['<', '>', '(', ')', ' '], ""); + let mut parts = sanitised.split(','); + + let x = parts + .next() + .and_then(|s| s.trim().parse::().ok()) + .ok_or_else(|| format!("{}: could not get x from {}", ERROR, s))?; + + let y = parts + .next() + .and_then(|s| s.trim().parse::().ok()) + .ok_or_else(|| format!("{}: could not get y from {}", ERROR, s))?; + + let radius = parts + .next() + .and_then(|s| s.trim().parse::().ok()) + .ok_or_else(|| format!("{}: could not get radius from {}", ERROR, s))?; + + if parts.next().is_some() { + return Err(format!("{}: too many numbers inputted in {}", ERROR, s).into()); + } + + if radius < 0. { + return Err(format!("{}: cannot have negative radius: {}", ERROR, s).into()); + } + + Ok(PgCircle { x, y, radius }) + } +} + +impl PgCircle { + fn from_bytes(mut bytes: &[u8]) -> Result { + let x = bytes.get_f64(); + let y = bytes.get_f64(); + let r = bytes.get_f64(); + Ok(PgCircle { x, y, radius: r }) + } + + fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), Error> { + buff.extend_from_slice(&self.x.to_be_bytes()); + buff.extend_from_slice(&self.y.to_be_bytes()); + buff.extend_from_slice(&self.radius.to_be_bytes()); + Ok(()) + } + + #[cfg(test)] + fn serialize_to_vec(&self) -> Vec { + let mut buff = PgArgumentBuffer::default(); + self.serialize(&mut buff).unwrap(); + buff.to_vec() + } +} + +#[cfg(test)] +mod circle_tests { + + use std::str::FromStr; + + use super::PgCircle; + + const CIRCLE_BYTES: &[u8] = &[ + 63, 241, 153, 153, 153, 153, 153, 154, 64, 1, 153, 153, 153, 153, 153, 154, 64, 10, 102, + 102, 102, 102, 102, 102, + ]; + + #[test] + fn can_deserialise_circle_type_bytes() { + let circle = PgCircle::from_bytes(CIRCLE_BYTES).unwrap(); + assert_eq!( + circle, + PgCircle { + x: 1.1, + y: 2.2, + radius: 3.3 + } + ) + } + + #[test] + fn can_deserialise_circle_type_str() { + let circle = PgCircle::from_str("<(1, 2), 3 >").unwrap(); + assert_eq!( + circle, + PgCircle { + x: 1.0, + y: 2.0, + radius: 3.0 + } + ); + } + + #[test] + fn can_deserialise_circle_type_str_second_syntax() { + let circle = PgCircle::from_str("((1, 2), 3 )").unwrap(); + assert_eq!( + circle, + PgCircle { + x: 1.0, + y: 2.0, + radius: 3.0 + } + ); + } + + #[test] + fn can_deserialise_circle_type_str_third_syntax() { + let circle = PgCircle::from_str("(1, 2), 3 ").unwrap(); + assert_eq!( + circle, + PgCircle { + x: 1.0, + y: 2.0, + radius: 3.0 + } + ); + } + + #[test] + fn can_deserialise_circle_type_str_fourth_syntax() { + let circle = PgCircle::from_str("1, 2, 3 ").unwrap(); + assert_eq!( + circle, + PgCircle { + x: 1.0, + y: 2.0, + radius: 3.0 + } + ); + } + + #[test] + fn cannot_deserialise_circle_invalid_numbers() { + let input_str = "1, 2, Three"; + let circle = PgCircle::from_str(input_str); + assert!(circle.is_err()); + if let Err(err) = circle { + assert_eq!( + err.to_string(), + format!("error decoding CIRCLE: could not get radius from {input_str}") + ) + } + } + + #[test] + fn cannot_deserialise_circle_negative_radius() { + let input_str = "1, 2, -3"; + let circle = PgCircle::from_str(input_str); + assert!(circle.is_err()); + if let Err(err) = circle { + assert_eq!( + err.to_string(), + format!("error decoding CIRCLE: cannot have negative radius: {input_str}") + ) + } + } + + #[test] + fn can_deserialise_circle_type_str_float() { + let circle = PgCircle::from_str("<(1.1, 2.2), 3.3>").unwrap(); + assert_eq!( + circle, + PgCircle { + x: 1.1, + y: 2.2, + radius: 3.3 + } + ); + } + + #[test] + fn can_serialise_circle_type() { + let circle = PgCircle { + x: 1.1, + y: 2.2, + radius: 3.3, + }; + assert_eq!(circle.serialize_to_vec(), CIRCLE_BYTES,) + } +} diff --git a/sqlx-postgres/src/types/geometry/line.rs b/sqlx-postgres/src/types/geometry/line.rs index 43f93c1c..8f08c949 100644 --- a/sqlx-postgres/src/types/geometry/line.rs +++ b/sqlx-postgres/src/types/geometry/line.rs @@ -15,7 +15,10 @@ const ERROR: &str = "error decoding LINE"; /// /// Lines are represented by the linear equation Ax + By + C = 0, where A and B are not both zero. /// -/// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-LINE +/// See [Postgres Manual, Section 8.8.2, Geometric Types - Lines][PG.S.8.8.2] for details. +/// +/// [PG.S.8.8.2]: https://www.postgresql.org/docs/current/datatype-geometric.html#DATATYPE-LINE +/// #[derive(Debug, Clone, PartialEq)] pub struct PgLine { pub a: f64, diff --git a/sqlx-postgres/src/types/geometry/line_segment.rs b/sqlx-postgres/src/types/geometry/line_segment.rs index 5dc5efc7..cd08e4da 100644 --- a/sqlx-postgres/src/types/geometry/line_segment.rs +++ b/sqlx-postgres/src/types/geometry/line_segment.rs @@ -23,7 +23,10 @@ const ERROR: &str = "error decoding LSEG"; /// ``` /// where `(start_x,start_y) and (end_x,end_y)` are the end points of the line segment. /// -/// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-LSEG +/// See [Postgres Manual, Section 8.8.3, Geometric Types - Line Segments][PG.S.8.8.3] for details. +/// +/// [PG.S.8.8.3]: https://www.postgresql.org/docs/current/datatype-geometric.html#DATATYPE-LSEG +/// #[doc(alias = "line segment")] #[derive(Debug, Clone, PartialEq)] pub struct PgLSeg { diff --git a/sqlx-postgres/src/types/geometry/mod.rs b/sqlx-postgres/src/types/geometry/mod.rs index 1437d72c..c3142145 100644 --- a/sqlx-postgres/src/types/geometry/mod.rs +++ b/sqlx-postgres/src/types/geometry/mod.rs @@ -1,4 +1,5 @@ pub mod r#box; +pub mod circle; pub mod line; pub mod line_segment; pub mod path; diff --git a/sqlx-postgres/src/types/geometry/path.rs b/sqlx-postgres/src/types/geometry/path.rs index 87a3b3e8..6799289f 100644 --- a/sqlx-postgres/src/types/geometry/path.rs +++ b/sqlx-postgres/src/types/geometry/path.rs @@ -27,7 +27,10 @@ const BYTE_WIDTH: usize = mem::size_of::(); /// where the points are the end points of the line segments comprising the path. Square brackets `([])` indicate an open path, while parentheses `(())` indicate a closed path. /// When the outermost parentheses are omitted, as in the third through fifth syntaxes, a closed path is assumed. /// -/// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-GEOMETRIC-PATHS +/// See [Postgres Manual, Section 8.8.5, Geometric Types - Paths][PG.S.8.8.5] for details. +/// +/// [PG.S.8.8.5]: https://www.postgresql.org/docs/current/datatype-geometric.html#DATATYPE-GEOMETRIC-PATHS +/// #[derive(Debug, Clone, PartialEq)] pub struct PgPath { pub closed: bool, diff --git a/sqlx-postgres/src/types/geometry/point.rs b/sqlx-postgres/src/types/geometry/point.rs index cc106729..83b7c24d 100644 --- a/sqlx-postgres/src/types/geometry/point.rs +++ b/sqlx-postgres/src/types/geometry/point.rs @@ -19,7 +19,10 @@ use std::str::FromStr; /// ```` /// where x and y are the respective coordinates, as floating-point numbers. /// -/// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-GEOMETRIC-POINTS +/// See [Postgres Manual, Section 8.8.1, Geometric Types - Points][PG.S.8.8.1] for details. +/// +/// [PG.S.8.8.1]: https://www.postgresql.org/docs/current/datatype-geometric.html#DATATYPE-GEOMETRIC-POINTS +/// #[derive(Debug, Clone, PartialEq)] pub struct PgPoint { pub x: f64, diff --git a/sqlx-postgres/src/types/geometry/polygon.rs b/sqlx-postgres/src/types/geometry/polygon.rs index 500c9933..a5a203c6 100644 --- a/sqlx-postgres/src/types/geometry/polygon.rs +++ b/sqlx-postgres/src/types/geometry/polygon.rs @@ -28,7 +28,10 @@ const BYTE_WIDTH: usize = mem::size_of::(); /// /// where the points are the end points of the line segments comprising the boundary of the polygon. /// -/// Seeh ttps://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-POLYGON +/// See [Postgres Manual, Section 8.8.6, Geometric Types - Polygons][PG.S.8.8.6] for details. +/// +/// [PG.S.8.8.6]: https://www.postgresql.org/docs/current/datatype-geometric.html#DATATYPE-POLYGON +/// #[derive(Debug, Clone, PartialEq)] pub struct PgPolygon { pub points: Vec, diff --git a/sqlx-postgres/src/types/mod.rs b/sqlx-postgres/src/types/mod.rs index 550ce629..c3493139 100644 --- a/sqlx-postgres/src/types/mod.rs +++ b/sqlx-postgres/src/types/mod.rs @@ -27,6 +27,7 @@ //! | [`PgBox`] | BOX | //! | [`PgPath`] | PATH | //! | [`PgPolygon`] | POLYGON | +//! | [`PgCircle`] | CIRCLE | //! | [`PgHstore`] | HSTORE | //! //! 1 SQLx generally considers `CITEXT` to be compatible with `String`, `&str`, etc., @@ -262,6 +263,7 @@ mod bit_vec; pub use array::PgHasArrayType; pub use citext::PgCiText; pub use cube::PgCube; +pub use geometry::circle::PgCircle; pub use geometry::line::PgLine; pub use geometry::line_segment::PgLSeg; pub use geometry::path::PgPath; diff --git a/tests/postgres/types.rs b/tests/postgres/types.rs index d88e1657..da20467e 100644 --- a/tests/postgres/types.rs +++ b/tests/postgres/types.rs @@ -539,6 +539,14 @@ test_type!(polygon(Postgres, ]}, )); +#[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] +test_type!(circle(Postgres, + "circle('<(1.1, -2.2), 3.3>')" ~= sqlx::postgres::types::PgCircle { x: 1.1, y:-2.2, radius: 3.3 }, + "circle('((1.1, -2.2), 3.3)')" ~= sqlx::postgres::types::PgCircle { x: 1.1, y:-2.2, radius: 3.3 }, + "circle('(1.1, -2.2), 3.3')" ~= sqlx::postgres::types::PgCircle { x: 1.1, y:-2.2, radius: 3.3 }, + "circle('1.1, -2.2, 3.3')" ~= sqlx::postgres::types::PgCircle { x: 1.1, y:-2.2, radius: 3.3 }, +)); + #[cfg(feature = "rust_decimal")] test_type!(decimal(Postgres, "0::numeric" == sqlx::types::Decimal::from_str("0").unwrap(),