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

Merged
merged 12 commits into from
Nov 27, 2024
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
14 changes: 14 additions & 0 deletions sqlx-test/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ 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($db:ident, $($text:literal == $value:expr),+ $(,)?)) => {
$crate::test_type!($name<$name>($db, $($text == $value),+));
};
Expand Down Expand Up @@ -82,6 +88,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 +230,10 @@ 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}::text is not distinct from $1::text)::int4, {0}, $2"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this necessary? What happens without it?

Copy link
Contributor Author

@jayy-lmao jayy-lmao Nov 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We get the error could not identify an equality operator for type point without it

We can't use the SELECT ({0} is not distinct from $1)::int4, {0}, $2 from https://github.com/jayy-lmao/sqlx/blob/feat/geometry-postgres-point/sqlx-test/src/lib.rs#L230 as the comparison

https://github.com/launchbadge/sqlx/actions/runs/11679795147/job/32521594696

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. = compares area for the two-dimensional types, and there's a different operator for equality, ~=.

https://www.postgresql.org/docs/current/functions-geometry.html

We should use that rather than converting from text because there could be differences in rounding between Postgres and Rust that alter the text output of the types.

Copy link
Contributor Author

@jayy-lmao jayy-lmao Nov 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Took a bit of a fiddle, and needed to split between array/non-array variants.

This is what worked in the end, text avoided: https://github.com/launchbadge/sqlx/pull/3583/files#diff-31a2fb7770cf7cd19a7baadb82c0ccfa1721d92c1eeb4d3f7f9a675de54f20b0R247-R251

};
}
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