Skip to content
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

feat: point #3583

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
2 changes: 2 additions & 0 deletions sqlx-postgres/src/type_checking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ impl_type_checking!(

sqlx::postgres::types::PgCube,

sqlx::postgres::types::PgPoint,

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

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
@@ -0,0 +1 @@
pub mod point;
138 changes: 138 additions & 0 deletions sqlx-postgres/src/types/geometry/point.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
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;

/// ## Postgres Geometric Point type
///
/// Description: Point on a plane
/// Representation: `(x, y)`
///
/// Points are the fundamental two-dimensional building block for geometric types. Values of type point are specified using either of the following syntaxes:
/// ```text
/// ( x , y )
/// x , y
/// ````
/// 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
#[derive(Debug, Clone, PartialEq)]
pub struct PgPoint {
pub x: f64,
pub y: f64,
}

impl Type<Postgres> for PgPoint {
fn type_info() -> PgTypeInfo {
PgTypeInfo::with_name("point")
}
}

impl PgHasArrayType for PgPoint {
fn array_type_info() -> PgTypeInfo {
PgTypeInfo::with_name("_point")
}
}

impl<'r> Decode<'r, Postgres> for PgPoint {
fn decode(value: PgValueRef<'r>) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
match value.format() {
PgValueFormat::Text => Ok(PgPoint::from_str(value.as_str()?)?),
PgValueFormat::Binary => Ok(PgPoint::from_bytes(value.as_bytes()?)?),
}
}
}

impl<'q> Encode<'q, Postgres> for PgPoint {
fn produces(&self) -> Option<PgTypeInfo> {
Some(PgTypeInfo::with_name("point"))
}

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

fn parse_float_from_str(s: &str, error_msg: &str) -> Result<f64, Error> {
s.trim()
.parse()
.map_err(|_| Error::Decode(error_msg.into()))
}

impl FromStr for PgPoint {
type Err = BoxDynError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let (x_str, y_str) = s
.trim_matches(|c| c == '(' || c == ')' || c == ' ')
.split_once(',')
.ok_or_else(|| format!("error decoding POINT: could not get x and y from {}", s))?;

let x = parse_float_from_str(x_str, "error decoding POINT: could not get x")?;
let y = parse_float_from_str(y_str, "error decoding POINT: could not get x")?;

Ok(PgPoint { x, y })
}
}

impl PgPoint {
fn from_bytes(mut bytes: &[u8]) -> Result<PgPoint, BoxDynError> {
let x = bytes.get_f64();
let y = bytes.get_f64();
Ok(PgPoint { x, y })
}

fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), BoxDynError> {
buff.extend_from_slice(&self.x.to_be_bytes());
buff.extend_from_slice(&self.y.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 point_tests {

use std::str::FromStr;

use super::PgPoint;

const POINT_BYTES: &[u8] = &[
64, 0, 204, 204, 204, 204, 204, 205, 64, 20, 204, 204, 204, 204, 204, 205,
];

#[test]
fn can_deserialise_point_type_bytes() {
let point = PgPoint::from_bytes(POINT_BYTES).unwrap();
assert_eq!(point, PgPoint { x: 2.1, y: 5.2 })
}

#[test]
fn can_deserialise_point_type_str() {
let point = PgPoint::from_str("(2, 3)").unwrap();
assert_eq!(point, PgPoint { x: 2., y: 3. });
}

#[test]
fn can_deserialise_point_type_str_float() {
let point = PgPoint::from_str("(2.5, 3.4)").unwrap();
assert_eq!(point, PgPoint { x: 2.5, y: 3.4 });
}

#[test]
fn can_serialise_point_type() {
let point = PgPoint { x: 2.1, y: 5.2 };
assert_eq!(point.serialize_to_vec(), POINT_BYTES,)
}
}
4 changes: 4 additions & 0 deletions sqlx-postgres/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
//! | [`PgLQuery`] | LQUERY |
//! | [`PgCiText`] | CITEXT<sup>1</sup> |
//! | [`PgCube`] | CUBE |
//! | [`PgPoint] | POINT |
//! | [`PgHstore`] | HSTORE |
//!
//! <sup>1</sup> SQLx generally considers `CITEXT` to be compatible with `String`, `&str`, etc.,
Expand Down Expand Up @@ -212,6 +213,8 @@ mod bigdecimal;

mod cube;

mod geometry;

#[cfg(any(feature = "bigdecimal", feature = "rust_decimal"))]
mod numeric;

Expand Down Expand Up @@ -242,6 +245,7 @@ mod bit_vec;
pub use array::PgHasArrayType;
pub use citext::PgCiText;
pub use cube::PgCube;
pub use geometry::point::PgPoint;
pub use hstore::PgHstore;
pub use interval::PgInterval;
pub use lquery::PgLQuery;
Expand Down
27 changes: 27 additions & 0 deletions sqlx-test/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,18 @@ macro_rules! test_type {
}
};

($name:ident<$ty:ty>($db:ident, $($text:literal ~= $value:expr),+ $(,)?)) => {
paste::item! {
$crate::__test_prepared_type!($name<$ty>($db, $crate::[< $db _query_for_test_prepared_geometric_type >]!(), $($text == $value),+));
}
};
($name:ident<$ty:ty>($db:ident, $($text:literal @= $value:expr),+ $(,)?)) => {
paste::item! {
$crate::__test_prepared_type!($name<$ty>($db, $crate::[< $db _query_for_test_prepared_geometric_array_type >]!(), $($text == $value),+));
}
};


($name:ident($db:ident, $($text:literal == $value:expr),+ $(,)?)) => {
$crate::test_type!($name<$name>($db, $($text == $value),+));
};
Expand Down Expand Up @@ -82,6 +94,7 @@ macro_rules! test_prepared_type {
}
};


($name:ident($db:ident, $($text:literal == $value:expr),+ $(,)?)) => {
$crate::__test_prepared_type!($name<$name>($db, $($text == $value),+));
};
Expand Down Expand Up @@ -223,3 +236,17 @@ macro_rules! Postgres_query_for_test_prepared_type {
"SELECT ({0} is not distinct from $1)::int4, {0}, $2"
};
}

#[macro_export]
macro_rules! Postgres_query_for_test_prepared_geometric_type {
() => {
"SELECT ({0} ~= $1)::int4, {0}, $2"
};
}

#[macro_export]
macro_rules! Postgres_query_for_test_prepared_geometric_array_type {
() => {
"SELECT (SELECT bool_and(geo1.geometry ~= geo2.geometry) FROM unnest({0}) WITH ORDINALITY AS geo1(geometry, idx) JOIN unnest($1) WITH ORDINALITY AS geo2(geometry, idx) ON geo1.idx = geo2.idx)::int4, {0}, $2"
};
}
11 changes: 11 additions & 0 deletions tests/postgres/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,17 @@ test_type!(_cube<Vec<sqlx::postgres::types::PgCube>>(Postgres,
"array[cube(2.2,-3.4)]" == vec![sqlx::postgres::types::PgCube::OneDimensionInterval(2.2, -3.4)],
));

#[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))]
test_type!(point<sqlx::postgres::types::PgPoint>(Postgres,
"point(2.2,-3.4)" ~= sqlx::postgres::types::PgPoint { x: 2.2, y:-3.4 },
));

#[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))]
test_type!(_point<Vec<sqlx::postgres::types::PgPoint>>(Postgres,
"array[point(2,3),point(2.1,3.4)]" @= vec![sqlx::postgres::types::PgPoint { x:2., y: 3. }, sqlx::postgres::types::PgPoint { x:2.1, y: 3.4 }],
"array[point(2.2,-3.4)]" @= vec![sqlx::postgres::types::PgPoint { x: 2.2, y: -3.4 }],
));

#[cfg(feature = "rust_decimal")]
test_type!(decimal<sqlx::types::Decimal>(Postgres,
"0::numeric" == sqlx::types::Decimal::from_str("0").unwrap(),
Expand Down