feat(postgres): point (#3583)

* 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
This commit is contained in:
James H. 2024-11-28 08:35:42 +11:00 committed by GitHub
parent 3e8952b0d4
commit a7f2928a1b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 183 additions and 0 deletions

View file

@ -32,6 +32,8 @@ impl_type_checking!(
sqlx::postgres::types::PgCube,
sqlx::postgres::types::PgPoint,
#[cfg(feature = "uuid")]
sqlx::types::Uuid,

View file

@ -0,0 +1 @@
pub mod point;

View file

@ -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,)
}
}

View file

@ -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.,
@ -212,6 +213,8 @@ mod bigdecimal;
mod cube;
mod geometry;
#[cfg(any(feature = "bigdecimal", feature = "rust_decimal"))]
mod numeric;
@ -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;

View file

@ -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),+));
};
@ -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),+));
};
@ -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"
};
}

View file

@ -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(),