Skip to content

Commit

Permalink
feat(postgres): point (#3583)
Browse files Browse the repository at this point in the history
* feat: point

* test: try if eq operator works for arrays of geometries

* fix: re-introduce comparison

* fix: test other geometry comparison

* test: geometry array equality check

* test: array match for geo arrays geo match for geo only

* fix: prepare geometric array type

* fix: update array comparison

* fix: try another method of geometric array comparison

* fix: one more geometry match tests

* fix: correct query syntax

* test: geometry test further
  • Loading branch information
jayy-lmao authored Nov 27, 2024
1 parent 3e8952b commit a7f2928
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 0 deletions.
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

0 comments on commit a7f2928

Please sign in to comment.