Skip to content

feat(postgres): add geometry circle #3773

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions sqlx-postgres/src/type_checking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ impl_type_checking!(

sqlx::postgres::types::PgPolygon,

sqlx::postgres::types::PgCircle,

#[cfg(feature = "uuid")]
sqlx::types::Uuid,

Expand Down
5 changes: 4 additions & 1 deletion sqlx-postgres/src/types/geometry/box.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
250 changes: 250 additions & 0 deletions sqlx-postgres/src/types/geometry/circle.rs
Original file line number Diff line number Diff line change
@@ -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<Postgres> 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<Self, Box<dyn std::error::Error + Send + Sync>> {
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<PgTypeInfo> {
Some(PgTypeInfo::with_name("circle"))
}

fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
self.serialize(buf)?;
Ok(IsNull::No)
}
}

impl FromStr for PgCircle {
type Err = BoxDynError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let sanitised = s.replace(['<', '>', '(', ')', ' '], "");
let mut parts = sanitised.split(',');

let x = parts
.next()
.and_then(|s| s.trim().parse::<f64>().ok())
.ok_or_else(|| format!("{}: could not get x from {}", ERROR, s))?;

let y = parts
.next()
.and_then(|s| s.trim().parse::<f64>().ok())
.ok_or_else(|| format!("{}: could not get y from {}", ERROR, s))?;

let radius = parts
.next()
.and_then(|s| s.trim().parse::<f64>().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<PgCircle, Error> {
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<u8> {
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,)
}
}
5 changes: 4 additions & 1 deletion sqlx-postgres/src/types/geometry/line.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 4 additions & 1 deletion sqlx-postgres/src/types/geometry/line_segment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions sqlx-postgres/src/types/geometry/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod r#box;
pub mod circle;
pub mod line;
pub mod line_segment;
pub mod path;
Expand Down
5 changes: 4 additions & 1 deletion sqlx-postgres/src/types/geometry/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ const BYTE_WIDTH: usize = mem::size_of::<f64>();
/// 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,
Expand Down
5 changes: 4 additions & 1 deletion sqlx-postgres/src/types/geometry/point.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 4 additions & 1 deletion sqlx-postgres/src/types/geometry/polygon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ const BYTE_WIDTH: usize = mem::size_of::<f64>();
///
/// 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<PgPoint>,
Expand Down
2 changes: 2 additions & 0 deletions sqlx-postgres/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
//! | [`PgBox`] | BOX |
//! | [`PgPath`] | PATH |
//! | [`PgPolygon`] | POLYGON |
//! | [`PgCircle`] | CIRCLE |
//! | [`PgHstore`] | HSTORE |
//!
//! <sup>1</sup> SQLx generally considers `CITEXT` to be compatible with `String`, `&str`, etc.,
Expand Down Expand Up @@ -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;
Expand Down
8 changes: 8 additions & 0 deletions tests/postgres/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,14 @@ test_type!(polygon<sqlx::postgres::types::PgPolygon>(Postgres,
]},
));

#[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))]
test_type!(circle<sqlx::postgres::types::PgCircle>(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<sqlx::types::Decimal>(Postgres,
"0::numeric" == sqlx::types::Decimal::from_str("0").unwrap(),
Expand Down