diff --git a/Cargo.lock b/Cargo.lock index 2da47afa54..36cb94422e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1177,7 +1177,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.87", ] [[package]] @@ -1914,7 +1914,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.87", ] [[package]] @@ -3986,7 +3986,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.87", ] [[package]] @@ -4815,7 +4815,7 @@ checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.87", "synstructure", ] @@ -4856,7 +4856,7 @@ checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.87", "synstructure", ] @@ -4899,5 +4899,5 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.87", ] diff --git a/sqlx-postgres/src/type_checking.rs b/sqlx-postgres/src/type_checking.rs index edefa54211..eb18c5a999 100644 --- a/sqlx-postgres/src/type_checking.rs +++ b/sqlx-postgres/src/type_checking.rs @@ -34,6 +34,8 @@ impl_type_checking!( sqlx::postgres::types::PgPoint, + sqlx::postgres::types::PgLine, + #[cfg(feature = "uuid")] sqlx::types::Uuid, diff --git a/sqlx-postgres/src/types/geometry/line.rs b/sqlx-postgres/src/types/geometry/line.rs new file mode 100644 index 0000000000..43f93c1c33 --- /dev/null +++ b/sqlx-postgres/src/types/geometry/line.rs @@ -0,0 +1,211 @@ +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 std::str::FromStr; + +const ERROR: &str = "error decoding LINE"; + +/// ## Postgres Geometric Line type +/// +/// Description: Infinite line +/// Representation: `{A, B, C}` +/// +/// 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 +#[derive(Debug, Clone, PartialEq)] +pub struct PgLine { + pub a: f64, + pub b: f64, + pub c: f64, +} + +impl Type for PgLine { + fn type_info() -> PgTypeInfo { + PgTypeInfo::with_name("line") + } +} + +impl PgHasArrayType for PgLine { + fn array_type_info() -> PgTypeInfo { + PgTypeInfo::with_name("_line") + } +} + +impl<'r> Decode<'r, Postgres> for PgLine { + fn decode(value: PgValueRef<'r>) -> Result> { + match value.format() { + PgValueFormat::Text => Ok(PgLine::from_str(value.as_str()?)?), + PgValueFormat::Binary => Ok(PgLine::from_bytes(value.as_bytes()?)?), + } + } +} + +impl<'q> Encode<'q, Postgres> for PgLine { + fn produces(&self) -> Option { + Some(PgTypeInfo::with_name("line")) + } + + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { + self.serialize(buf)?; + Ok(IsNull::No) + } +} + +impl FromStr for PgLine { + type Err = BoxDynError; + + fn from_str(s: &str) -> Result { + let mut parts = s + .trim_matches(|c| c == '{' || c == '}' || c == ' ') + .split(','); + + let a = parts + .next() + .and_then(|s| s.trim().parse::().ok()) + .ok_or_else(|| format!("{}: could not get a from {}", ERROR, s))?; + + let b = parts + .next() + .and_then(|s| s.trim().parse::().ok()) + .ok_or_else(|| format!("{}: could not get b from {}", ERROR, s))?; + + let c = parts + .next() + .and_then(|s| s.trim().parse::().ok()) + .ok_or_else(|| format!("{}: could not get c from {}", ERROR, s))?; + + if parts.next().is_some() { + return Err(format!("{}: too many numbers inputted in {}", ERROR, s).into()); + } + + Ok(PgLine { a, b, c }) + } +} + +impl PgLine { + fn from_bytes(mut bytes: &[u8]) -> Result { + let a = bytes.get_f64(); + let b = bytes.get_f64(); + let c = bytes.get_f64(); + Ok(PgLine { a, b, c }) + } + + fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), BoxDynError> { + buff.extend_from_slice(&self.a.to_be_bytes()); + buff.extend_from_slice(&self.b.to_be_bytes()); + buff.extend_from_slice(&self.c.to_be_bytes()); + Ok(()) + } + + #[cfg(test)] + fn serialize_to_vec(&self) -> Vec { + let mut buff = PgArgumentBuffer::default(); + self.serialize(&mut buff).unwrap(); + buff.to_vec() + } +} + +#[cfg(test)] +mod line_tests { + + use std::str::FromStr; + + use super::PgLine; + + const LINE_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_line_type_bytes() { + let line = PgLine::from_bytes(LINE_BYTES).unwrap(); + assert_eq!( + line, + PgLine { + a: 1.1, + b: 2.2, + c: 3.3 + } + ) + } + + #[test] + fn can_deserialise_line_type_str() { + let line = PgLine::from_str("{ 1, 2, 3 }").unwrap(); + assert_eq!( + line, + PgLine { + a: 1.0, + b: 2.0, + c: 3.0 + } + ); + } + + #[test] + fn cannot_deserialise_line_too_few_numbers() { + let input_str = "{ 1, 2 }"; + let line = PgLine::from_str(input_str); + assert!(line.is_err()); + if let Err(err) = line { + assert_eq!( + err.to_string(), + format!("error decoding LINE: could not get c from {input_str}") + ) + } + } + + #[test] + fn cannot_deserialise_line_too_many_numbers() { + let input_str = "{ 1, 2, 3, 4 }"; + let line = PgLine::from_str(input_str); + assert!(line.is_err()); + if let Err(err) = line { + assert_eq!( + err.to_string(), + format!("error decoding LINE: too many numbers inputted in {input_str}") + ) + } + } + + #[test] + fn cannot_deserialise_line_invalid_numbers() { + let input_str = "{ 1, 2, three }"; + let line = PgLine::from_str(input_str); + assert!(line.is_err()); + if let Err(err) = line { + assert_eq!( + err.to_string(), + format!("error decoding LINE: could not get c from {input_str}") + ) + } + } + + #[test] + fn can_deserialise_line_type_str_float() { + let line = PgLine::from_str("{1.1, 2.2, 3.3}").unwrap(); + assert_eq!( + line, + PgLine { + a: 1.1, + b: 2.2, + c: 3.3 + } + ); + } + + #[test] + fn can_serialise_line_type() { + let line = PgLine { + a: 1.1, + b: 2.2, + c: 3.3, + }; + assert_eq!(line.serialize_to_vec(), LINE_BYTES,) + } +} diff --git a/sqlx-postgres/src/types/geometry/mod.rs b/sqlx-postgres/src/types/geometry/mod.rs index a199ff7517..daf9f1deb9 100644 --- a/sqlx-postgres/src/types/geometry/mod.rs +++ b/sqlx-postgres/src/types/geometry/mod.rs @@ -1 +1,2 @@ +pub mod line; pub mod point; diff --git a/sqlx-postgres/src/types/mod.rs b/sqlx-postgres/src/types/mod.rs index 2a571de265..26feb05580 100644 --- a/sqlx-postgres/src/types/mod.rs +++ b/sqlx-postgres/src/types/mod.rs @@ -22,6 +22,7 @@ //! | [`PgCiText`] | CITEXT1 | //! | [`PgCube`] | CUBE | //! | [`PgPoint] | POINT | +//! | [`PgLine] | LINE | //! | [`PgHstore`] | HSTORE | //! //! 1 SQLx generally considers `CITEXT` to be compatible with `String`, `&str`, etc., @@ -245,6 +246,7 @@ mod bit_vec; pub use array::PgHasArrayType; pub use citext::PgCiText; pub use cube::PgCube; +pub use geometry::line::PgLine; pub use geometry::point::PgPoint; pub use hstore::PgHstore; pub use interval::PgInterval; diff --git a/tests/postgres/types.rs b/tests/postgres/types.rs index 4d10d66d8f..42c50ed2b3 100644 --- a/tests/postgres/types.rs +++ b/tests/postgres/types.rs @@ -503,6 +503,17 @@ test_type!(_point>(Postgres, "array[point(2.2,-3.4)]" @= vec![sqlx::postgres::types::PgPoint { x: 2.2, y: -3.4 }], )); +#[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] +test_type!(line(Postgres, + "line('{1.1, -2.2, 3.3}')" @= sqlx::postgres::types::PgLine { a: 1.1, b:-2.2, c: 3.3 }, + "line('((0.0, 0.0), (1.0,1.0))')" @= sqlx::postgres::types::PgLine { a: 1., b: -1., c: 0. }, +)); + +#[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] +test_type!(_line>(Postgres, + "array[line('{1,2,3}'),line('{1.1, 2.2, 3.3}')]" @= vec![sqlx::postgres::types::PgLine { a:1., b: 2., c: 3. }, sqlx::postgres::types::PgLine { a:1.1, b: 2.2, c: 3.3 }], +)); + #[cfg(feature = "rust_decimal")] test_type!(decimal(Postgres, "0::numeric" == sqlx::types::Decimal::from_str("0").unwrap(),