From 9653de9445f267170c8d9c0e1cf7fe6c525a7f04 Mon Sep 17 00:00:00 2001 From: James Holman Date: Tue, 3 Dec 2024 18:29:31 +1100 Subject: [PATCH 1/6] feat: add geometry line --- Cargo.lock | 12 +- sqlx-postgres/src/type_checking.rs | 2 + sqlx-postgres/src/types/geometry/line.rs | 211 +++++++++++++++++++++++ sqlx-postgres/src/types/geometry/mod.rs | 1 + sqlx-postgres/src/types/mod.rs | 2 + tests/postgres/types.rs | 11 ++ 6 files changed, 233 insertions(+), 6 deletions(-) create mode 100644 sqlx-postgres/src/types/geometry/line.rs 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(), From bda1c91f7407b1e4a25e6a6e93076b7d0d529341 Mon Sep 17 00:00:00 2001 From: James Holman Date: Wed, 4 Dec 2024 07:53:30 +1100 Subject: [PATCH 2/6] fix: point vs line --- sqlx-test/src/lib.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/sqlx-test/src/lib.rs b/sqlx-test/src/lib.rs index cc77f38dba..fad4afe32b 100644 --- a/sqlx-test/src/lib.rs +++ b/sqlx-test/src/lib.rs @@ -247,6 +247,16 @@ macro_rules! Postgres_query_for_test_prepared_geometric_type { #[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" + "SELECT (SELECT bool_and( + case + when pg_typeof(p1.geo)::text = 'point' then + p1.geo[0] = p2.geo[0] + and p1.geo[1] = p2.geo[1] + when pg_typeof(p1.geo)::text = 'line' then + p1.geo[0] = p2.geo[0] + and p1.geo[1] = p2.geo[1] + and p1.geo[2] = p2.geo[2] + end + ) 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" }; } From e7bab22a07031925cf54cf302f981bd3845d3bc3 Mon Sep 17 00:00:00 2001 From: James Holman Date: Wed, 4 Dec 2024 08:32:57 +1100 Subject: [PATCH 3/6] fix: try regular comparison for line --- tests/postgres/types.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/postgres/types.rs b/tests/postgres/types.rs index 42c50ed2b3..bcc4be0421 100644 --- a/tests/postgres/types.rs +++ b/tests/postgres/types.rs @@ -505,8 +505,8 @@ test_type!(_point>(Postgres, #[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. }, + "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))] From 542c6408957dc2a114fbb9bfa27f8c97d84b144c Mon Sep 17 00:00:00 2001 From: James Holman Date: Wed, 4 Dec 2024 08:41:05 +1100 Subject: [PATCH 4/6] fix: undo point comparison change --- sqlx-test/src/lib.rs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/sqlx-test/src/lib.rs b/sqlx-test/src/lib.rs index fad4afe32b..cc77f38dba 100644 --- a/sqlx-test/src/lib.rs +++ b/sqlx-test/src/lib.rs @@ -247,16 +247,6 @@ macro_rules! Postgres_query_for_test_prepared_geometric_type { #[macro_export] macro_rules! Postgres_query_for_test_prepared_geometric_array_type { () => { - "SELECT (SELECT bool_and( - case - when pg_typeof(p1.geo)::text = 'point' then - p1.geo[0] = p2.geo[0] - and p1.geo[1] = p2.geo[1] - when pg_typeof(p1.geo)::text = 'line' then - p1.geo[0] = p2.geo[0] - and p1.geo[1] = p2.geo[1] - and p1.geo[2] = p2.geo[2] - end - ) 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" + "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" }; } From d76822f0a18b7ae724e72f75f1ec5d82b3f6a96d Mon Sep 17 00:00:00 2001 From: James Holman Date: Wed, 4 Dec 2024 08:41:28 +1100 Subject: [PATCH 5/6] fix: regular comparison for array lines --- tests/postgres/types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/postgres/types.rs b/tests/postgres/types.rs index bcc4be0421..2ab32509d8 100644 --- a/tests/postgres/types.rs +++ b/tests/postgres/types.rs @@ -511,7 +511,7 @@ test_type!(line(Postgres, #[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 }], + "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")] From 43d946ac209c1f5cdc2e7e67ccaa2bb2cce7c6cc Mon Sep 17 00:00:00 2001 From: James Holman Date: Wed, 4 Dec 2024 19:00:33 +1100 Subject: [PATCH 6/6] fix: remove line array test --- tests/postgres/types.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/postgres/types.rs b/tests/postgres/types.rs index 2ab32509d8..3f6c362043 100644 --- a/tests/postgres/types.rs +++ b/tests/postgres/types.rs @@ -509,11 +509,6 @@ test_type!(line(Postgres, "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(),