From 4248c948cc64f932dd4b37cb3adfbcdf108eeee9 Mon Sep 17 00:00:00 2001 From: James Holman Date: Sat, 24 Aug 2024 14:23:32 +1000 Subject: [PATCH 01/64] fix: clean branch --- sqlx-postgres/src/types/geometry/mod.rs | 5 + sqlx-postgres/src/types/geometry/path.rs | 214 +++++++++++++++++++++++ tests/postgres/types.rs | 42 +++++ 3 files changed, 261 insertions(+) create mode 100644 sqlx-postgres/src/types/geometry/mod.rs create mode 100644 sqlx-postgres/src/types/geometry/path.rs diff --git a/sqlx-postgres/src/types/geometry/mod.rs b/sqlx-postgres/src/types/geometry/mod.rs new file mode 100644 index 0000000000..f67846fef2 --- /dev/null +++ b/sqlx-postgres/src/types/geometry/mod.rs @@ -0,0 +1,5 @@ +pub mod r#box; +pub mod line; +pub mod line_segment; +pub mod path; +pub mod point; diff --git a/sqlx-postgres/src/types/geometry/path.rs b/sqlx-postgres/src/types/geometry/path.rs new file mode 100644 index 0000000000..b90f05af6c --- /dev/null +++ b/sqlx-postgres/src/types/geometry/path.rs @@ -0,0 +1,214 @@ +use crate::decode::Decode; +use crate::encode::{Encode, IsNull}; +use crate::error::BoxDynError; +use crate::types::{PgPoint, Type}; +use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, Postgres}; +use sqlx_core::Error; +use std::str::FromStr; + +const BYTE_WIDTH: usize = 8; + +/// Postgres Geometric Path type +/// +/// Storage size: 16+16n bytes +/// Description: Open path or Closed path (similar to polygon) +/// Representation: ((x1,y1),(x2,y2)) +/// +/// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-GEOMETRIC-PATH +#[derive(Debug, Clone, PartialEq)] +pub struct PgPath { + pub points: Vec, +} + +impl Type for PgPath { + fn type_info() -> PgTypeInfo { + PgTypeInfo::with_name("path") + } +} + +impl PgHasArrayType for PgPath { + fn array_type_info() -> PgTypeInfo { + PgTypeInfo::with_name("_path") + } +} + +impl<'r> Decode<'r, Postgres> for PgPath { + fn decode(value: PgValueRef<'r>) -> Result> { + match value.format() { + PgValueFormat::Text => Ok(PgPath::from_str(value.as_str()?)?), + PgValueFormat::Binary => Ok(pg_path_from_bytes(value.as_bytes()?)?), + } + } +} + +impl<'q> Encode<'q, Postgres> for PgPath { + fn produces(&self) -> Option { + Some(PgTypeInfo::with_name("path")) + } + + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { + self.serialize(buf)?; + Ok(IsNull::No) + } +} + +impl FromStr for PgPath { + type Err = Error; + + fn from_str(s: &str) -> Result { + let sanitised = s.replace(&['(', ')', '[', ']', ' '][..], ""); + let mut parts = sanitised.splitn(4, ","); + + let mut points = vec![]; + + while let (Some(x_str), Some(y_str)) = (parts.next(), parts.next()) { + let x = parse_float_from_str(x_str, "could not get x")?; + let y = parse_float_from_str(y_str, "could not get y")?; + + let point = PgPoint { x, y }; + points.push(point); + } + + if !points.is_empty() { + return Ok(PgPath { points }); + } + + Err(Error::Decode( + format!("could not get path from {}", s).into(), + )) + } +} + +fn pg_path_from_bytes(bytes: &[u8]) -> Result { + let mut points = vec![]; + + let steps = bytes.len() / BYTE_WIDTH; + + for n in (0..steps).step_by(2) { + let x = get_f64_from_bytes(bytes, BYTE_WIDTH * n)?; + let y = get_f64_from_bytes(bytes, BYTE_WIDTH * (n + 1))?; + points.push(PgPoint { x, y }) + } + + Ok(PgPath { points }) +} + +impl PgPath { + fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), Error> { + for point in &self.points { + buff.extend_from_slice(&point.x.to_be_bytes()); + buff.extend_from_slice(&point.y.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() + } +} + +fn get_f64_from_bytes(bytes: &[u8], start: usize) -> Result { + bytes + .get(start..start + BYTE_WIDTH) + .ok_or(Error::Decode( + format!("Could not decode path bytes: {:?}", bytes).into(), + ))? + .try_into() + .map(f64::from_be_bytes) + .map_err(|err| Error::Decode(format!("Invalid bytes slice: {:?}", err).into())) +} + +fn parse_float_from_str(s: &str, error_msg: &str) -> Result { + s.parse().map_err(|_| Error::Decode(error_msg.into())) +} + +#[cfg(test)] +mod path_tests { + + use std::str::FromStr; + + use crate::types::PgPoint; + + use super::{pg_path_from_bytes, PgPath}; + + const LINE_SEGMENT_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, 64, 17, 153, 153, 153, 153, 153, 154, + ]; + + #[test] + fn can_deserialise_path_type_byes() { + let path = pg_path_from_bytes(LINE_SEGMENT_BYTES).unwrap(); + assert_eq!( + path, + PgPath { + points: vec![PgPoint { x: 1.1, y: 2.2 }, PgPoint { x: 3.3, y: 4.4 }] + } + ) + } + + #[test] + fn can_deserialise_path_type_str_first_syntax() { + let path = PgPath::from_str("[( 1, 2), (3, 4 )]").unwrap(); + assert_eq!( + path, + PgPath { + points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }] + } + ); + } + #[test] + fn can_deserialise_path_type_str_second_syntax() { + let path = PgPath::from_str("(( 1, 2), (3, 4 ))").unwrap(); + assert_eq!( + path, + PgPath { + points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }] + } + ); + } + + #[test] + fn can_deserialise_path_type_str_third_syntax() { + let path = PgPath::from_str("(1, 2), (3, 4 )").unwrap(); + assert_eq!( + path, + PgPath { + points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }] + } + ); + } + + #[test] + fn can_deserialise_path_type_str_fourth_syntax() { + let path = PgPath::from_str("1, 2, 3, 4").unwrap(); + assert_eq!( + path, + PgPath { + points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }] + } + ); + } + + #[test] + fn can_deserialise_path_type_str_float() { + let path = PgPath::from_str("(1.1, 2.2), (3.3, 4.4)").unwrap(); + assert_eq!( + path, + PgPath { + points: vec![PgPoint { x: 1.1, y: 2.2 }, PgPoint { x: 3.3, y: 4.4 }] + } + ); + } + + #[test] + fn can_serialise_path_type() { + let path = PgPath { + points: vec![PgPoint { x: 1.1, y: 2.2 }, PgPoint { x: 3.3, y: 4.4 }], + }; + assert_eq!(path.serialize_to_vec(), LINE_SEGMENT_BYTES,) + } +} diff --git a/tests/postgres/types.rs b/tests/postgres/types.rs index 4912339dc2..d4d5dea785 100644 --- a/tests/postgres/types.rs +++ b/tests/postgres/types.rs @@ -492,6 +492,48 @@ test_type!(_cube>(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(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>(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(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(any(postgres_12, postgres_13, postgres_14, postgres_15))] +test_type!(lseg(Postgres, + "lseg('((1.0, 2.0), (3.0,4.0))')" @= sqlx::postgres::types::PgLSeg { x1: 1., y1: 2., x2: 3. , y2: 4.}, +)); + +#[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] +test_type!(_lseg>(Postgres, + "array[lseg('(1,2,3,4)'),lseg('[(1.1, 2.2), (3.3, 4.4)]')]" @= vec![sqlx::postgres::types::PgLSeg { x1: 1., y1: 2., x2: 3., y2: 4 }, sqlx::postgres::types::PgLSeg { x1: 1.1, y1: 2.2, x2: 3.3, y2: 4.4 }], +)); + +#[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] +test_type!(box(Postgres, + "box('((1.0, 2.0), (3.0,4.0))')" @= sqlx::postgres::types::PgBox { x1: 1., y1: 2., x2: 3. , y2: 4.}, +)); + +#[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] +test_type!(_box>(Postgres, + "array[box('(1,2,3,4)'),box('[(1.1, 2.2), (3.3, 4.4)]')]" @= vec![sqlx::postgres::types::PgBox { x1: 1., y1: 2., x2: 3., y2: 4 }, sqlx::postgres::types::Pgbox { x1: 1.1, y1: 2.2, x2: 3.3, y2: 4.4 }], +)); + #[cfg(feature = "rust_decimal")] test_type!(decimal(Postgres, "0::numeric" == sqlx::types::Decimal::from_str("0").unwrap(), From d16c98e76a2d0509f2f3f763234ffc1ab80ddd30 Mon Sep 17 00:00:00 2001 From: James Holman Date: Sat, 24 Aug 2024 20:40:00 +1000 Subject: [PATCH 02/64] feat: points and boxes --- sqlx-postgres/src/types/geometry/box.rs | 228 ++++++++++++++++++ sqlx-postgres/src/types/geometry/line.rs | 180 ++++++++++++++ .../src/types/geometry/line_segment.rs | 228 ++++++++++++++++++ sqlx-postgres/src/types/geometry/path.rs | 70 +++++- sqlx-postgres/src/types/geometry/point.rs | 146 +++++++++++ 5 files changed, 839 insertions(+), 13 deletions(-) create mode 100644 sqlx-postgres/src/types/geometry/box.rs create mode 100644 sqlx-postgres/src/types/geometry/line.rs create mode 100644 sqlx-postgres/src/types/geometry/line_segment.rs create mode 100644 sqlx-postgres/src/types/geometry/point.rs diff --git a/sqlx-postgres/src/types/geometry/box.rs b/sqlx-postgres/src/types/geometry/box.rs new file mode 100644 index 0000000000..b8e762e969 --- /dev/null +++ b/sqlx-postgres/src/types/geometry/box.rs @@ -0,0 +1,228 @@ +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::Error; +use std::str::FromStr; + +const BYTE_WIDTH: usize = 8; + +/// Postgres Geometric Box type +/// +/// Storage size: 32 bytes +/// Description: Rectangular box +/// Representation: ((x1,y1),(x2,y2)) +/// +/// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-GEOMETRIC-BOXES +#[derive(Debug, Clone, PartialEq)] +pub struct PgBox { + pub x1: f64, + pub y1: f64, + pub x2: f64, + pub y2: f64, +} + +impl Type for PgBox { + fn type_info() -> PgTypeInfo { + PgTypeInfo::with_name("box") + } +} + +impl PgHasArrayType for PgBox { + fn array_type_info() -> PgTypeInfo { + PgTypeInfo::with_name("_box") + } +} + +impl<'r> Decode<'r, Postgres> for PgBox { + fn decode(value: PgValueRef<'r>) -> Result> { + match value.format() { + PgValueFormat::Text => Ok(PgBox::from_str(value.as_str()?)?), + PgValueFormat::Binary => Ok(pg_box_from_bytes(value.as_bytes()?)?), + } + } +} + +impl<'q> Encode<'q, Postgres> for PgBox { + fn produces(&self) -> Option { + Some(PgTypeInfo::with_name("box")) + } + + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { + self.serialize(buf)?; + Ok(IsNull::No) + } +} + +impl FromStr for PgBox { + type Err = Error; + + fn from_str(s: &str) -> Result { + let sanitised = s.replace(&['(', ')', '[', ']', ' '][..], ""); + let mut parts = sanitised.splitn(4, ","); + + if let (Some(x1_str), Some(y1_str), Some(x2_str), Some(y2_str)) = + (parts.next(), parts.next(), parts.next(), parts.next()) + { + let x1 = parse_float_from_str(x1_str, "could not get x1")?; + let y1 = parse_float_from_str(y1_str, "could not get y1")?; + let x2 = parse_float_from_str(x2_str, "could not get x2")?; + let y2 = parse_float_from_str(y2_str, "could not get y2")?; + + return Ok(PgBox { x1, y1, x2, y2 }); + } + + Err(Error::Decode( + format!("could not get x1, y1, x2, y2 from {}", s).into(), + )) + } +} + +fn pg_box_from_bytes(bytes: &[u8]) -> Result { + let x1 = get_f64_from_bytes(bytes, 0)?; + let y1 = get_f64_from_bytes(bytes, BYTE_WIDTH)?; + let x2 = get_f64_from_bytes(bytes, BYTE_WIDTH * 2)?; + let y2 = get_f64_from_bytes(bytes, BYTE_WIDTH * 3)?; + + Ok(PgBox { x1, y1, x2, y2 }) +} + +impl PgBox { + fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), Error> { + buff.extend_from_slice(&self.x1.to_be_bytes()); + buff.extend_from_slice(&self.y1.to_be_bytes()); + buff.extend_from_slice(&self.x2.to_be_bytes()); + buff.extend_from_slice(&self.y2.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() + } +} + +fn get_f64_from_bytes(bytes: &[u8], start: usize) -> Result { + bytes + .get(start..start + BYTE_WIDTH) + .ok_or(Error::Decode( + format!("Could not decode box bytes: {:?}", bytes).into(), + ))? + .try_into() + .map(f64::from_be_bytes) + .map_err(|err| Error::Decode(format!("Invalid bytes slice: {:?}", err).into())) +} + +fn parse_float_from_str(s: &str, error_msg: &str) -> Result { + s.parse().map_err(|_| Error::Decode(error_msg.into())) +} + +#[cfg(test)] +mod box_tests { + + use std::str::FromStr; + + use super::{pg_box_from_bytes, PgBox}; + + const BOX_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, 64, 17, 153, 153, 153, 153, 153, 154, + ]; + + #[test] + fn can_deserialise_box_type_bytes() { + let pg_box = pg_box_from_bytes(BOX_BYTES).unwrap(); + assert_eq!( + pg_box, + PgBox { + x1: 1.1, + y1: 2.2, + x2: 3.3, + y2: 4.4 + } + ) + } + + #[test] + fn can_deserialise_box_type_str_first_syntax() { + let pg_box = PgBox::from_str("[( 1, 2), (3, 4 )]").unwrap(); + assert_eq!( + pg_box, + PgBox { + x1: 1., + y1: 2., + x2: 3., + y2: 4. + } + ); + } + #[test] + fn can_deserialise_box_type_str_second_syntax() { + let pg_box = PgBox::from_str("(( 1, 2), (3, 4 ))").unwrap(); + assert_eq!( + pg_box, + PgBox { + x1: 1., + y1: 2., + x2: 3., + y2: 4. + } + ); + } + + #[test] + fn can_deserialise_box_type_str_third_syntax() { + let pg_box = PgBox::from_str("(1, 2), (3, 4 )").unwrap(); + assert_eq!( + pg_box, + PgBox { + x1: 1., + y1: 2., + x2: 3., + y2: 4. + } + ); + } + + #[test] + fn can_deserialise_box_type_str_fourth_syntax() { + let pg_box = PgBox::from_str("1, 2, 3, 4").unwrap(); + assert_eq!( + pg_box, + PgBox { + x1: 1., + y1: 2., + x2: 3., + y2: 4. + } + ); + } + + #[test] + fn can_deserialise_box_type_str_float() { + let pg_box = PgBox::from_str("(1.1, 2.2), (3.3, 4.4)").unwrap(); + assert_eq!( + pg_box, + PgBox { + x1: 1.1, + y1: 2.2, + x2: 3.3, + y2: 4.4 + } + ); + } + + #[test] + fn can_serialise_box_type() { + let pg_box = PgBox { + x1: 1.1, + y1: 2.2, + x2: 3.3, + y2: 4.4, + }; + assert_eq!(pg_box.serialize_to_vec(), BOX_BYTES,) + } +} diff --git a/sqlx-postgres/src/types/geometry/line.rs b/sqlx-postgres/src/types/geometry/line.rs new file mode 100644 index 0000000000..3486feb713 --- /dev/null +++ b/sqlx-postgres/src/types/geometry/line.rs @@ -0,0 +1,180 @@ +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::Error; +use std::str::FromStr; + +const BYTE_WIDTH: usize = 8; + +/// Postgres Geometric Line type +/// +/// Storage size: 24 bytes +/// Description: Infinite line +/// Representation: {A, B, C} +/// +/// 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(pg_line_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 = Error; + + fn from_str(s: &str) -> Result { + let mut parts = s + .trim_matches(|c| c == '{' || c == '}' || c == ' ') + .splitn(3, ','); + + if let (Some(a_str), Some(b_str), Some(c_str)) = (parts.next(), parts.next(), parts.next()) + { + let a = parse_float_from_str(a_str, "could not get A")?; + let b = parse_float_from_str(b_str, "could not get B")?; + let c = parse_float_from_str(c_str, "could not get C")?; + + return Ok(PgLine { a, b, c }); + } + + Err(Error::Decode( + format!("could not get A,B,C from {}", s).into(), + )) + } +} + +fn pg_line_from_bytes(bytes: &[u8]) -> Result { + let a = get_f64_from_bytes(bytes, 0)?; + let b = get_f64_from_bytes(bytes, BYTE_WIDTH)?; + let c = get_f64_from_bytes(bytes, BYTE_WIDTH * 2)?; + Ok(PgLine { a, b, c }) +} + +impl PgLine { + fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), Error> { + 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() + } +} + +fn get_f64_from_bytes(bytes: &[u8], start: usize) -> Result { + bytes + .get(start..start + BYTE_WIDTH) + .ok_or(Error::Decode( + format!("Could not decode line bytes: {:?}", bytes).into(), + ))? + .try_into() + .map(f64::from_be_bytes) + .map_err(|err| Error::Decode(format!("Invalid bytes slice: {:?}", err).into())) +} + +fn parse_float_from_str(s: &str, error_msg: &str) -> Result { + s.trim() + .parse() + .map_err(|_| Error::Decode(error_msg.into())) +} + +#[cfg(test)] +mod line_tests { + + use std::str::FromStr; + + use super::{pg_line_from_bytes, 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 = pg_line_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 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/line_segment.rs b/sqlx-postgres/src/types/geometry/line_segment.rs new file mode 100644 index 0000000000..ee608f43dc --- /dev/null +++ b/sqlx-postgres/src/types/geometry/line_segment.rs @@ -0,0 +1,228 @@ +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::Error; +use std::str::FromStr; + +const BYTE_WIDTH: usize = 8; + +/// Postgres Geometric Line Segment type +/// +/// Storage size: 32 bytes +/// Description: Finite line segment +/// Representation: ((x1,y1),(x2,y2)) +/// +/// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-LSEG +#[derive(Debug, Clone, PartialEq)] +pub struct PgLSeg { + pub x1: f64, + pub y1: f64, + pub x2: f64, + pub y2: f64, +} + +impl Type for PgLSeg { + fn type_info() -> PgTypeInfo { + PgTypeInfo::with_name("lseg") + } +} + +impl PgHasArrayType for PgLSeg { + fn array_type_info() -> PgTypeInfo { + PgTypeInfo::with_name("_lseg") + } +} + +impl<'r> Decode<'r, Postgres> for PgLSeg { + fn decode(value: PgValueRef<'r>) -> Result> { + match value.format() { + PgValueFormat::Text => Ok(PgLSeg::from_str(value.as_str()?)?), + PgValueFormat::Binary => Ok(pg_lseg_from_bytes(value.as_bytes()?)?), + } + } +} + +impl<'q> Encode<'q, Postgres> for PgLSeg { + fn produces(&self) -> Option { + Some(PgTypeInfo::with_name("lseg")) + } + + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { + self.serialize(buf)?; + Ok(IsNull::No) + } +} + +impl FromStr for PgLSeg { + type Err = Error; + + fn from_str(s: &str) -> Result { + let sanitised = s.replace(&['(', ')', '[', ']', ' '][..], ""); + let mut parts = sanitised.splitn(4, ","); + + if let (Some(x1_str), Some(y1_str), Some(x2_str), Some(y2_str)) = + (parts.next(), parts.next(), parts.next(), parts.next()) + { + let x1 = parse_float_from_str(x1_str, "could not get x1")?; + let y1 = parse_float_from_str(y1_str, "could not get y1")?; + let x2 = parse_float_from_str(x2_str, "could not get x2")?; + let y2 = parse_float_from_str(y2_str, "could not get y2")?; + + return Ok(PgLSeg { x1, y1, x2, y2 }); + } + + Err(Error::Decode( + format!("could not get x1, y1, x2, y2 from {}", s).into(), + )) + } +} + +fn pg_lseg_from_bytes(bytes: &[u8]) -> Result { + let x1 = get_f64_from_bytes(bytes, 0)?; + let y1 = get_f64_from_bytes(bytes, BYTE_WIDTH)?; + let x2 = get_f64_from_bytes(bytes, BYTE_WIDTH * 2)?; + let y2 = get_f64_from_bytes(bytes, BYTE_WIDTH * 3)?; + + Ok(PgLSeg { x1, y1, x2, y2 }) +} + +impl PgLSeg { + fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), Error> { + buff.extend_from_slice(&self.x1.to_be_bytes()); + buff.extend_from_slice(&self.y1.to_be_bytes()); + buff.extend_from_slice(&self.x2.to_be_bytes()); + buff.extend_from_slice(&self.y2.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() + } +} + +fn get_f64_from_bytes(bytes: &[u8], start: usize) -> Result { + bytes + .get(start..start + BYTE_WIDTH) + .ok_or(Error::Decode( + format!("Could not decode lseg bytes: {:?}", bytes).into(), + ))? + .try_into() + .map(f64::from_be_bytes) + .map_err(|err| Error::Decode(format!("Invalid bytes slice: {:?}", err).into())) +} + +fn parse_float_from_str(s: &str, error_msg: &str) -> Result { + s.parse().map_err(|_| Error::Decode(error_msg.into())) +} + +#[cfg(test)] +mod lseg_tests { + + use std::str::FromStr; + + use super::{pg_lseg_from_bytes, PgLSeg}; + + const LINE_SEGMENT_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, 64, 17, 153, 153, 153, 153, 153, 154, + ]; + + #[test] + fn can_deserialise_lseg_type_bytes() { + let lseg = pg_lseg_from_bytes(LINE_SEGMENT_BYTES).unwrap(); + assert_eq!( + lseg, + PgLSeg { + x1: 1.1, + y1: 2.2, + x2: 3.3, + y2: 4.4 + } + ) + } + + #[test] + fn can_deserialise_lseg_type_str_first_syntax() { + let lseg = PgLSeg::from_str("[( 1, 2), (3, 4 )]").unwrap(); + assert_eq!( + lseg, + PgLSeg { + x1: 1., + y1: 2., + x2: 3., + y2: 4. + } + ); + } + #[test] + fn can_deserialise_lseg_type_str_second_syntax() { + let lseg = PgLSeg::from_str("(( 1, 2), (3, 4 ))").unwrap(); + assert_eq!( + lseg, + PgLSeg { + x1: 1., + y1: 2., + x2: 3., + y2: 4. + } + ); + } + + #[test] + fn can_deserialise_lseg_type_str_third_syntax() { + let lseg = PgLSeg::from_str("(1, 2), (3, 4 )").unwrap(); + assert_eq!( + lseg, + PgLSeg { + x1: 1., + y1: 2., + x2: 3., + y2: 4. + } + ); + } + + #[test] + fn can_deserialise_lseg_type_str_fourth_syntax() { + let lseg = PgLSeg::from_str("1, 2, 3, 4").unwrap(); + assert_eq!( + lseg, + PgLSeg { + x1: 1., + y1: 2., + x2: 3., + y2: 4. + } + ); + } + + #[test] + fn can_deserialise_lseg_type_str_float() { + let lseg = PgLSeg::from_str("(1.1, 2.2), (3.3, 4.4)").unwrap(); + assert_eq!( + lseg, + PgLSeg { + x1: 1.1, + y1: 2.2, + x2: 3.3, + y2: 4.4 + } + ); + } + + #[test] + fn can_serialise_lseg_type() { + let lseg = PgLSeg { + x1: 1.1, + y1: 2.2, + x2: 3.3, + y2: 4.4, + }; + assert_eq!(lseg.serialize_to_vec(), LINE_SEGMENT_BYTES,) + } +} diff --git a/sqlx-postgres/src/types/geometry/path.rs b/sqlx-postgres/src/types/geometry/path.rs index b90f05af6c..7424acd2d6 100644 --- a/sqlx-postgres/src/types/geometry/path.rs +++ b/sqlx-postgres/src/types/geometry/path.rs @@ -17,6 +17,7 @@ const BYTE_WIDTH: usize = 8; /// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-GEOMETRIC-PATH #[derive(Debug, Clone, PartialEq)] pub struct PgPath { + pub closed: bool, pub points: Vec, } @@ -56,6 +57,7 @@ impl FromStr for PgPath { type Err = Error; fn from_str(s: &str) -> Result { + let closed = !s.contains("["); let sanitised = s.replace(&['(', ')', '[', ']', ' '][..], ""); let mut parts = sanitised.splitn(4, ","); @@ -70,7 +72,7 @@ impl FromStr for PgPath { } if !points.is_empty() { - return Ok(PgPath { points }); + return Ok(PgPath { points, closed }); } Err(Error::Decode( @@ -82,19 +84,26 @@ impl FromStr for PgPath { fn pg_path_from_bytes(bytes: &[u8]) -> Result { let mut points = vec![]; - let steps = bytes.len() / BYTE_WIDTH; + let offset: usize = 5; + let closed = get_i8_from_bytes(bytes, 0)? == 1i8; + let steps = (bytes.len() - offset) / BYTE_WIDTH; for n in (0..steps).step_by(2) { - let x = get_f64_from_bytes(bytes, BYTE_WIDTH * n)?; - let y = get_f64_from_bytes(bytes, BYTE_WIDTH * (n + 1))?; + let x = get_f64_from_bytes(bytes, BYTE_WIDTH * n + offset)?; + let y = get_f64_from_bytes(bytes, BYTE_WIDTH * (n + 1) + offset)?; points.push(PgPoint { x, y }) } - Ok(PgPath { points }) + Ok(PgPath { points, closed }) } impl PgPath { fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), Error> { + let closed = self.closed as i8; + let length = self.points.len() as i32; + buff.extend_from_slice(&closed.to_be_bytes()); + buff.extend_from_slice(&length.to_be_bytes()); + for point in &self.points { buff.extend_from_slice(&point.x.to_be_bytes()); buff.extend_from_slice(&point.y.to_be_bytes()); @@ -110,6 +119,17 @@ impl PgPath { } } +fn get_i8_from_bytes(bytes: &[u8], start: usize) -> Result { + bytes + .get(start..start + 1) + .ok_or(Error::Decode( + format!("Could not decode path bytes: {:?}", bytes).into(), + ))? + .try_into() + .map(i8::from_be_bytes) + .map_err(|err| Error::Decode(format!("Invalid bytes slice: {:?}", err).into())) +} + fn get_f64_from_bytes(bytes: &[u8], start: usize) -> Result { bytes .get(start..start + BYTE_WIDTH) @@ -134,18 +154,36 @@ mod path_tests { use super::{pg_path_from_bytes, PgPath}; - const LINE_SEGMENT_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, 64, 17, 153, 153, 153, 153, 153, 154, + const PATH_CLOSED_BYTES: &[u8] = &[ + 1, 0, 0, 0, 2, 63, 240, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 64, 8, 0, 0, 0, 0, 0, 0, + 64, 16, 0, 0, 0, 0, 0, 0, + ]; + + const PATH_OPEN_BYTES: &[u8] = &[ + 0, 0, 0, 0, 2, 63, 240, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 64, 8, 0, 0, 0, 0, 0, 0, + 64, 16, 0, 0, 0, 0, 0, 0, ]; #[test] - fn can_deserialise_path_type_byes() { - let path = pg_path_from_bytes(LINE_SEGMENT_BYTES).unwrap(); + fn can_deserialise_path_type_bytes_closed() { + let path = pg_path_from_bytes(PATH_CLOSED_BYTES).unwrap(); assert_eq!( path, PgPath { - points: vec![PgPoint { x: 1.1, y: 2.2 }, PgPoint { x: 3.3, y: 4.4 }] + closed: true, + points: vec![PgPoint { x: 1.0, y: 2.0 }, PgPoint { x: 3.0, y: 4.0 }] + } + ) + } + + #[test] + fn can_deserialise_path_type_bytes_open() { + let path = pg_path_from_bytes(PATH_OPEN_BYTES).unwrap(); + assert_eq!( + path, + PgPath { + closed: false, + points: vec![PgPoint { x: 1.0, y: 2.0 }, PgPoint { x: 3.0, y: 4.0 }] } ) } @@ -156,6 +194,7 @@ mod path_tests { assert_eq!( path, PgPath { + closed: false, points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }] } ); @@ -166,6 +205,7 @@ mod path_tests { assert_eq!( path, PgPath { + closed: true, points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }] } ); @@ -177,6 +217,7 @@ mod path_tests { assert_eq!( path, PgPath { + closed: true, points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }] } ); @@ -188,6 +229,7 @@ mod path_tests { assert_eq!( path, PgPath { + closed: true, points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }] } ); @@ -199,6 +241,7 @@ mod path_tests { assert_eq!( path, PgPath { + closed: true, points: vec![PgPoint { x: 1.1, y: 2.2 }, PgPoint { x: 3.3, y: 4.4 }] } ); @@ -207,8 +250,9 @@ mod path_tests { #[test] fn can_serialise_path_type() { let path = PgPath { - points: vec![PgPoint { x: 1.1, y: 2.2 }, PgPoint { x: 3.3, y: 4.4 }], + closed: true, + points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }], }; - assert_eq!(path.serialize_to_vec(), LINE_SEGMENT_BYTES,) + assert_eq!(path.serialize_to_vec(), PATH_CLOSED_BYTES,) } } diff --git a/sqlx-postgres/src/types/geometry/point.rs b/sqlx-postgres/src/types/geometry/point.rs new file mode 100644 index 0000000000..2d2aefba53 --- /dev/null +++ b/sqlx-postgres/src/types/geometry/point.rs @@ -0,0 +1,146 @@ +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::Error; +use std::str::FromStr; + +const BYTE_WIDTH: usize = 8; + +/// Postgres Geometric Point type +/// +/// Storage size: 16 bytes +/// Description: Point on a plane +/// Representation: (x, y) +/// +/// 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 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> { + match value.format() { + PgValueFormat::Text => Ok(PgPoint::from_str(value.as_str()?)?), + PgValueFormat::Binary => Ok(pg_point_from_bytes(value.as_bytes()?)?), + } + } +} + +impl<'q> Encode<'q, Postgres> for PgPoint { + fn produces(&self) -> Option { + Some(PgTypeInfo::with_name("point")) + } + + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { + self.serialize(buf)?; + Ok(IsNull::No) + } +} + +impl FromStr for PgPoint { + type Err = Error; + + fn from_str(s: &str) -> Result { + let (x_str, y_str) = s + .trim_matches(|c| c == '(' || c == ')' || c == ' ') + .split_once(',') + .ok_or(Error::Decode( + format!("could not get x and y from {}", s).into(), + ))?; + + let x = parse_float_from_str(x_str, "could not get x")?; + let y = parse_float_from_str(y_str, "could not get x")?; + + Ok(PgPoint { x, y }) + } +} + +fn pg_point_from_bytes(bytes: &[u8]) -> Result { + let x = get_f64_from_bytes(bytes, 0)?; + let y = get_f64_from_bytes(bytes, 8)?; + Ok(PgPoint { x, y }) +} + +impl PgPoint { + fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), Error> { + 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 { + let mut buff = PgArgumentBuffer::default(); + self.serialize(&mut buff).unwrap(); + buff.to_vec() + } +} + +fn get_f64_from_bytes(bytes: &[u8], start: usize) -> Result { + bytes + .get(start..start + BYTE_WIDTH) + .ok_or(Error::Decode( + format!("Could not decode point bytes: {:?}", bytes).into(), + ))? + .try_into() + .map(f64::from_be_bytes) + .map_err(|err| Error::Decode(format!("Invalid bytes slice: {:?}", err).into())) +} + +fn parse_float_from_str(s: &str, error_msg: &str) -> Result { + s.trim() + .parse() + .map_err(|_| Error::Decode(error_msg.into())) +} + +#[cfg(test)] +mod point_tests { + + use std::str::FromStr; + + use super::{pg_point_from_bytes, 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 = pg_point_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,) + } +} From 1c5387c2a8c64e1996e0b6bcdec73ca250110e74 Mon Sep 17 00:00:00 2001 From: James Holman Date: Sat, 24 Aug 2024 20:46:04 +1000 Subject: [PATCH 03/64] test: geo array test support --- sqlx-test/src/lib.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/sqlx-test/src/lib.rs b/sqlx-test/src/lib.rs index 5ba0f6323f..304492e695 100644 --- a/sqlx-test/src/lib.rs +++ b/sqlx-test/src/lib.rs @@ -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),+)); }; @@ -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),+)); }; @@ -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" + }; +} From d13fc3c6af24c9ac61896828b390a3e540b6f8ea Mon Sep 17 00:00:00 2001 From: James Holman Date: Sat, 24 Aug 2024 20:47:16 +1000 Subject: [PATCH 04/64] test: path tests --- tests/postgres/types.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/postgres/types.rs b/tests/postgres/types.rs index d4d5dea785..60035eed27 100644 --- a/tests/postgres/types.rs +++ b/tests/postgres/types.rs @@ -534,6 +534,17 @@ test_type!(_box>(Postgres, "array[box('(1,2,3,4)'),box('[(1.1, 2.2), (3.3, 4.4)]')]" @= vec![sqlx::postgres::types::PgBox { x1: 1., y1: 2., x2: 3., y2: 4 }, sqlx::postgres::types::Pgbox { x1: 1.1, y1: 2.2, x2: 3.3, y2: 4.4 }], )); +#[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] +test_type!(path(Postgres, + "path('((1.0, 2.0), (3.0,4.0))')" @= sqlx::postgres::types::PgPath { closed: true, points: vec![ sqlx::postgres::types::PgPoint { x: 1., y: 2. }, sqlx::postgres::types::PgPoint { x: 3. , y: 4. } ]}, + "path('[(1.0, 2.0), (3.0,4.0)]')" @= sqlx::postgres::types::PgPath { closed: false, points: vec![ sqlx::postgres::types::PgPoint { x: 1., y: 2. }, sqlx::postgres::types::PgPoint { x: 3. , y: 4. } ]}, +)); + +#[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] +test_type!(_path>(Postgres, + "array[path('(1,2),(3,4)'),path('[(1.1, 2.2), (3.3, 4.4)]')]" @= vec![sqlx::postgres::types::PgPath { closed: true, points: vec![ sqlx::postgres::types::PgPoint { x: 1., y: 2. }, sqlx::postgres::types::PgPoint { x: 3. , y: 4. } ]}, sqlx::postgres::types::PgPath { closed: false, points: vec![ sqlx::postgres::types::PgPoint { x: 1.1, y: 2.2 }, sqlx::postgres::types::PgPoint { x: 3.3 , y: 4.4 } ]},], +)); + #[cfg(feature = "rust_decimal")] test_type!(decimal(Postgres, "0::numeric" == sqlx::types::Decimal::from_str("0").unwrap(), From 01ea1fa9ca02220956757fb0ad2255bee247c89e Mon Sep 17 00:00:00 2001 From: James Holman Date: Sat, 24 Aug 2024 20:50:23 +1000 Subject: [PATCH 05/64] fix: imports --- sqlx-postgres/src/type_checking.rs | 10 ++++++++++ sqlx-postgres/src/types/mod.rs | 12 ++++++++++++ 2 files changed, 22 insertions(+) diff --git a/sqlx-postgres/src/type_checking.rs b/sqlx-postgres/src/type_checking.rs index e22d3b9007..6258aca4ae 100644 --- a/sqlx-postgres/src/type_checking.rs +++ b/sqlx-postgres/src/type_checking.rs @@ -32,6 +32,16 @@ impl_type_checking!( sqlx::postgres::types::PgCube, + sqlx::postgres::types::PgPoint, + + sqlx::postgres::types::PgLine, + + sqlx::postgres::types::PgLSeg, + + sqlx::postgres::types::PgBox, + + sqlx::postgres::types::PgPath, + #[cfg(feature = "uuid")] sqlx::types::Uuid, diff --git a/sqlx-postgres/src/types/mod.rs b/sqlx-postgres/src/types/mod.rs index 846f1b731d..9afb5bca44 100644 --- a/sqlx-postgres/src/types/mod.rs +++ b/sqlx-postgres/src/types/mod.rs @@ -21,6 +21,11 @@ //! | [`PgLQuery`] | LQUERY | //! | [`PgCiText`] | CITEXT1 | //! | [`PgCube`] | CUBE | +//! | [`PgPoint] | POINT | +//! | [`PgLine`] | LINE | +//! | [`PgLSeg`] | LSEG | +//! | [`PgBox`] | BOX | +//! | [`PgPath`] | PATH | //! | [`PgHstore`] | HSTORE | //! //! 1 SQLx generally considers `CITEXT` to be compatible with `String`, `&str`, etc., @@ -212,6 +217,8 @@ mod bigdecimal; mod cube; +mod geometry; + #[cfg(any(feature = "bigdecimal", feature = "rust_decimal"))] mod numeric; @@ -242,6 +249,11 @@ mod bit_vec; pub use array::PgHasArrayType; pub use citext::PgCiText; pub use cube::PgCube; +pub use geometry::line::PgLine; +pub use geometry::line_segment::PgLSeg; +pub use geometry::path::PgPath; +pub use geometry::point::PgPoint; +pub use geometry::r#box::PgBox; pub use hstore::PgHstore; pub use interval::PgInterval; pub use lquery::PgLQuery; From 1a64ebb0118f55cf0c29831029f828bf0f8a92de Mon Sep 17 00:00:00 2001 From: James Holman Date: Tue, 27 Aug 2024 18:30:45 +1000 Subject: [PATCH 06/64] fix: impl point --- sqlx-postgres/src/types/geometry/point.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/sqlx-postgres/src/types/geometry/point.rs b/sqlx-postgres/src/types/geometry/point.rs index 2d2aefba53..0b7c578f05 100644 --- a/sqlx-postgres/src/types/geometry/point.rs +++ b/sqlx-postgres/src/types/geometry/point.rs @@ -37,7 +37,7 @@ impl<'r> Decode<'r, Postgres> for PgPoint { fn decode(value: PgValueRef<'r>) -> Result> { match value.format() { PgValueFormat::Text => Ok(PgPoint::from_str(value.as_str()?)?), - PgValueFormat::Binary => Ok(pg_point_from_bytes(value.as_bytes()?)?), + PgValueFormat::Binary => Ok(PgPoint::from_bytes(value.as_bytes()?)?), } } } @@ -71,13 +71,13 @@ impl FromStr for PgPoint { } } -fn pg_point_from_bytes(bytes: &[u8]) -> Result { - let x = get_f64_from_bytes(bytes, 0)?; - let y = get_f64_from_bytes(bytes, 8)?; - Ok(PgPoint { x, y }) -} - impl PgPoint { + fn from_bytes(bytes: &[u8]) -> Result { + let x = get_f64_from_bytes(bytes, 0)?; + let y = get_f64_from_bytes(bytes, 8)?; + Ok(PgPoint { x, y }) + } + fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), Error> { buff.extend_from_slice(&self.x.to_be_bytes()); buff.extend_from_slice(&self.y.to_be_bytes()); @@ -114,7 +114,7 @@ mod point_tests { use std::str::FromStr; - use super::{pg_point_from_bytes, PgPoint}; + use super::PgPoint; const POINT_BYTES: &[u8] = &[ 64, 0, 204, 204, 204, 204, 204, 205, 64, 20, 204, 204, 204, 204, 204, 205, @@ -122,7 +122,7 @@ mod point_tests { #[test] fn can_deserialise_point_type_bytes() { - let point = pg_point_from_bytes(POINT_BYTES).unwrap(); + let point = PgPoint::from_bytes(POINT_BYTES).unwrap(); assert_eq!(point, PgPoint { x: 2.1, y: 5.2 }) } From 98dfbdf6187bf052fc45bfbe9c6cc97def098159 Mon Sep 17 00:00:00 2001 From: James Holman Date: Wed, 28 Aug 2024 20:14:57 +1000 Subject: [PATCH 07/64] fix: update point --- sqlx-postgres/src/types/geometry/point.rs | 36 ++++++++--------------- 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/sqlx-postgres/src/types/geometry/point.rs b/sqlx-postgres/src/types/geometry/point.rs index 0b7c578f05..9fdbaa8d2b 100644 --- a/sqlx-postgres/src/types/geometry/point.rs +++ b/sqlx-postgres/src/types/geometry/point.rs @@ -3,11 +3,10 @@ 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; -const BYTE_WIDTH: usize = 8; - /// Postgres Geometric Point type /// /// Storage size: 16 bytes @@ -53,6 +52,12 @@ impl<'q> Encode<'q, Postgres> for PgPoint { } } +fn parse_float_from_str(s: &str, error_msg: &str) -> Result { + s.trim() + .parse() + .map_err(|_| Error::Decode(error_msg.into())) +} + impl FromStr for PgPoint { type Err = Error; @@ -61,7 +66,7 @@ impl FromStr for PgPoint { .trim_matches(|c| c == '(' || c == ')' || c == ' ') .split_once(',') .ok_or(Error::Decode( - format!("could not get x and y from {}", s).into(), + format!("error decoding POINT: could not get x and y from {}", s).into(), ))?; let x = parse_float_from_str(x_str, "could not get x")?; @@ -72,13 +77,13 @@ impl FromStr for PgPoint { } impl PgPoint { - fn from_bytes(bytes: &[u8]) -> Result { - let x = get_f64_from_bytes(bytes, 0)?; - let y = get_f64_from_bytes(bytes, 8)?; + fn from_bytes(mut bytes: &[u8]) -> Result { + let x = bytes.get_f64(); + let y = bytes.get_f64(); Ok(PgPoint { x, y }) } - fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), Error> { + fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), String> { buff.extend_from_slice(&self.x.to_be_bytes()); buff.extend_from_slice(&self.y.to_be_bytes()); Ok(()) @@ -92,23 +97,6 @@ impl PgPoint { } } -fn get_f64_from_bytes(bytes: &[u8], start: usize) -> Result { - bytes - .get(start..start + BYTE_WIDTH) - .ok_or(Error::Decode( - format!("Could not decode point bytes: {:?}", bytes).into(), - ))? - .try_into() - .map(f64::from_be_bytes) - .map_err(|err| Error::Decode(format!("Invalid bytes slice: {:?}", err).into())) -} - -fn parse_float_from_str(s: &str, error_msg: &str) -> Result { - s.trim() - .parse() - .map_err(|_| Error::Decode(error_msg.into())) -} - #[cfg(test)] mod point_tests { From a69378c58de0ec9100fd24b8d073f73de59b1d86 Mon Sep 17 00:00:00 2001 From: James Holman Date: Wed, 28 Aug 2024 20:46:05 +1000 Subject: [PATCH 08/64] fix: line and line seg --- sqlx-postgres/src/types/geometry/line.rs | 74 +++++++++---------- .../src/types/geometry/line_segment.rs | 71 +++++++++--------- sqlx-postgres/src/types/geometry/point.rs | 4 +- 3 files changed, 72 insertions(+), 77 deletions(-) diff --git a/sqlx-postgres/src/types/geometry/line.rs b/sqlx-postgres/src/types/geometry/line.rs index 3486feb713..b1b1a81210 100644 --- a/sqlx-postgres/src/types/geometry/line.rs +++ b/sqlx-postgres/src/types/geometry/line.rs @@ -3,10 +3,11 @@ 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; -const BYTE_WIDTH: usize = 8; +const ERROR: &str = "error decoding LINE"; /// Postgres Geometric Line type /// @@ -38,7 +39,7 @@ 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(pg_line_from_bytes(value.as_bytes()?)?), + PgValueFormat::Binary => Ok(PgLine::from_bytes(value.as_bytes()?)?), } } } @@ -62,29 +63,39 @@ impl FromStr for PgLine { .trim_matches(|c| c == '{' || c == '}' || c == ' ') .splitn(3, ','); - if let (Some(a_str), Some(b_str), Some(c_str)) = (parts.next(), parts.next(), parts.next()) - { - let a = parse_float_from_str(a_str, "could not get A")?; - let b = parse_float_from_str(b_str, "could not get B")?; - let c = parse_float_from_str(c_str, "could not get C")?; - - return Ok(PgLine { a, b, c }); - } - - Err(Error::Decode( - format!("could not get A,B,C from {}", s).into(), - )) + let a = parts + .next() + .and_then(|s| s.trim().parse::().ok()) + .ok_or(Error::Decode( + format!("{}: could not get a from {}", ERROR, s).into(), + ))?; + + let b = parts + .next() + .and_then(|s| s.trim().parse::().ok()) + .ok_or(Error::Decode( + format!("{}: could not get b from {}", ERROR, s).into(), + ))?; + + let c = parts + .next() + .and_then(|s| s.trim().parse::().ok()) + .ok_or(Error::Decode( + format!("{}: could not get c from {}", ERROR, s).into(), + ))?; + + Ok(PgLine { a, b, c }) } } -fn pg_line_from_bytes(bytes: &[u8]) -> Result { - let a = get_f64_from_bytes(bytes, 0)?; - let b = get_f64_from_bytes(bytes, BYTE_WIDTH)?; - let c = get_f64_from_bytes(bytes, BYTE_WIDTH * 2)?; - 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<(), Error> { buff.extend_from_slice(&self.a.to_be_bytes()); buff.extend_from_slice(&self.b.to_be_bytes()); @@ -100,29 +111,12 @@ impl PgLine { } } -fn get_f64_from_bytes(bytes: &[u8], start: usize) -> Result { - bytes - .get(start..start + BYTE_WIDTH) - .ok_or(Error::Decode( - format!("Could not decode line bytes: {:?}", bytes).into(), - ))? - .try_into() - .map(f64::from_be_bytes) - .map_err(|err| Error::Decode(format!("Invalid bytes slice: {:?}", err).into())) -} - -fn parse_float_from_str(s: &str, error_msg: &str) -> Result { - s.trim() - .parse() - .map_err(|_| Error::Decode(error_msg.into())) -} - #[cfg(test)] mod line_tests { use std::str::FromStr; - use super::{pg_line_from_bytes, PgLine}; + 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, @@ -131,7 +125,7 @@ mod line_tests { #[test] fn can_deserialise_line_type_bytes() { - let line = pg_line_from_bytes(LINE_BYTES).unwrap(); + let line = PgLine::from_bytes(LINE_BYTES).unwrap(); assert_eq!( line, PgLine { diff --git a/sqlx-postgres/src/types/geometry/line_segment.rs b/sqlx-postgres/src/types/geometry/line_segment.rs index ee608f43dc..f8b45710cc 100644 --- a/sqlx-postgres/src/types/geometry/line_segment.rs +++ b/sqlx-postgres/src/types/geometry/line_segment.rs @@ -3,10 +3,11 @@ 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; -const BYTE_WIDTH: usize = 8; +const ERROR: &str = "error decoding LSEG"; /// Postgres Geometric Line Segment type /// @@ -62,28 +63,43 @@ impl FromStr for PgLSeg { let sanitised = s.replace(&['(', ')', '[', ']', ' '][..], ""); let mut parts = sanitised.splitn(4, ","); - if let (Some(x1_str), Some(y1_str), Some(x2_str), Some(y2_str)) = - (parts.next(), parts.next(), parts.next(), parts.next()) - { - let x1 = parse_float_from_str(x1_str, "could not get x1")?; - let y1 = parse_float_from_str(y1_str, "could not get y1")?; - let x2 = parse_float_from_str(x2_str, "could not get x2")?; - let y2 = parse_float_from_str(y2_str, "could not get y2")?; - - return Ok(PgLSeg { x1, y1, x2, y2 }); - } - - Err(Error::Decode( - format!("could not get x1, y1, x2, y2 from {}", s).into(), - )) + let x1 = parts + .next() + .and_then(|s| s.parse::().ok()) + .ok_or(Error::Decode( + format!("{}: could not get x1 from {}", ERROR, s).into(), + ))?; + + let y1 = parts + .next() + .and_then(|s| s.parse::().ok()) + .ok_or(Error::Decode( + format!("{}: could not get y1 from {}", ERROR, s).into(), + ))?; + + let x2 = parts + .next() + .and_then(|s| s.parse::().ok()) + .ok_or(Error::Decode( + format!("{}: could not get x2 from {}", ERROR, s).into(), + ))?; + + let y2 = parts + .next() + .and_then(|s| s.parse::().ok()) + .ok_or(Error::Decode( + format!("{}: could not get y2 from {}", ERROR, s).into(), + ))?; + + Ok(PgLSeg { x1, y1, x2, y2 }) } } -fn pg_lseg_from_bytes(bytes: &[u8]) -> Result { - let x1 = get_f64_from_bytes(bytes, 0)?; - let y1 = get_f64_from_bytes(bytes, BYTE_WIDTH)?; - let x2 = get_f64_from_bytes(bytes, BYTE_WIDTH * 2)?; - let y2 = get_f64_from_bytes(bytes, BYTE_WIDTH * 3)?; +fn pg_lseg_from_bytes(mut bytes: &[u8]) -> Result { + let x1 = bytes.get_f64(); + let y1 = bytes.get_f64(); + let x2 = bytes.get_f64(); + let y2 = bytes.get_f64(); Ok(PgLSeg { x1, y1, x2, y2 }) } @@ -105,21 +121,6 @@ impl PgLSeg { } } -fn get_f64_from_bytes(bytes: &[u8], start: usize) -> Result { - bytes - .get(start..start + BYTE_WIDTH) - .ok_or(Error::Decode( - format!("Could not decode lseg bytes: {:?}", bytes).into(), - ))? - .try_into() - .map(f64::from_be_bytes) - .map_err(|err| Error::Decode(format!("Invalid bytes slice: {:?}", err).into())) -} - -fn parse_float_from_str(s: &str, error_msg: &str) -> Result { - s.parse().map_err(|_| Error::Decode(error_msg.into())) -} - #[cfg(test)] mod lseg_tests { diff --git a/sqlx-postgres/src/types/geometry/point.rs b/sqlx-postgres/src/types/geometry/point.rs index 9fdbaa8d2b..a60bb39bd4 100644 --- a/sqlx-postgres/src/types/geometry/point.rs +++ b/sqlx-postgres/src/types/geometry/point.rs @@ -69,8 +69,8 @@ impl FromStr for PgPoint { format!("error decoding POINT: could not get x and y from {}", s).into(), ))?; - let x = parse_float_from_str(x_str, "could not get x")?; - let y = parse_float_from_str(y_str, "could not get x")?; + 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 }) } From 845cadd82374e3c063d34b9e7fee9dd97f24353a Mon Sep 17 00:00:00 2001 From: James Holman Date: Wed, 28 Aug 2024 20:49:04 +1000 Subject: [PATCH 09/64] fix: box --- sqlx-postgres/src/types/geometry/box.rs | 83 +++++++++++++------------ 1 file changed, 42 insertions(+), 41 deletions(-) diff --git a/sqlx-postgres/src/types/geometry/box.rs b/sqlx-postgres/src/types/geometry/box.rs index b8e762e969..8b93af13ce 100644 --- a/sqlx-postgres/src/types/geometry/box.rs +++ b/sqlx-postgres/src/types/geometry/box.rs @@ -3,10 +3,11 @@ 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; -const BYTE_WIDTH: usize = 8; +const ERROR: &str = "error decoding BOX"; /// Postgres Geometric Box type /// @@ -39,7 +40,7 @@ impl<'r> Decode<'r, Postgres> for PgBox { fn decode(value: PgValueRef<'r>) -> Result> { match value.format() { PgValueFormat::Text => Ok(PgBox::from_str(value.as_str()?)?), - PgValueFormat::Binary => Ok(pg_box_from_bytes(value.as_bytes()?)?), + PgValueFormat::Binary => Ok(PgBox::from_bytes(value.as_bytes()?)?), } } } @@ -62,33 +63,48 @@ impl FromStr for PgBox { let sanitised = s.replace(&['(', ')', '[', ']', ' '][..], ""); let mut parts = sanitised.splitn(4, ","); - if let (Some(x1_str), Some(y1_str), Some(x2_str), Some(y2_str)) = - (parts.next(), parts.next(), parts.next(), parts.next()) - { - let x1 = parse_float_from_str(x1_str, "could not get x1")?; - let y1 = parse_float_from_str(y1_str, "could not get y1")?; - let x2 = parse_float_from_str(x2_str, "could not get x2")?; - let y2 = parse_float_from_str(y2_str, "could not get y2")?; - - return Ok(PgBox { x1, y1, x2, y2 }); - } - - Err(Error::Decode( - format!("could not get x1, y1, x2, y2 from {}", s).into(), - )) + let x1 = parts + .next() + .and_then(|s| s.parse::().ok()) + .ok_or(Error::Decode( + format!("{}: could not get x1 from {}", ERROR, s).into(), + ))?; + + let y1 = parts + .next() + .and_then(|s| s.parse::().ok()) + .ok_or(Error::Decode( + format!("{}: could not get y1 from {}", ERROR, s).into(), + ))?; + + let x2 = parts + .next() + .and_then(|s| s.parse::().ok()) + .ok_or(Error::Decode( + format!("{}: could not get x2 from {}", ERROR, s).into(), + ))?; + + let y2 = parts + .next() + .and_then(|s| s.parse::().ok()) + .ok_or(Error::Decode( + format!("{}: could not get y2 from {}", ERROR, s).into(), + ))?; + + Ok(PgBox { x1, y1, x2, y2 }) } } -fn pg_box_from_bytes(bytes: &[u8]) -> Result { - let x1 = get_f64_from_bytes(bytes, 0)?; - let y1 = get_f64_from_bytes(bytes, BYTE_WIDTH)?; - let x2 = get_f64_from_bytes(bytes, BYTE_WIDTH * 2)?; - let y2 = get_f64_from_bytes(bytes, BYTE_WIDTH * 3)?; +impl PgBox { + fn from_bytes(mut bytes: &[u8]) -> Result { + let x1 = bytes.get_f64(); + let y1 = bytes.get_f64(); + let x2 = bytes.get_f64(); + let y2 = bytes.get_f64(); - Ok(PgBox { x1, y1, x2, y2 }) -} + Ok(PgBox { x1, y1, x2, y2 }) + } -impl PgBox { fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), Error> { buff.extend_from_slice(&self.x1.to_be_bytes()); buff.extend_from_slice(&self.y1.to_be_bytes()); @@ -105,27 +121,12 @@ impl PgBox { } } -fn get_f64_from_bytes(bytes: &[u8], start: usize) -> Result { - bytes - .get(start..start + BYTE_WIDTH) - .ok_or(Error::Decode( - format!("Could not decode box bytes: {:?}", bytes).into(), - ))? - .try_into() - .map(f64::from_be_bytes) - .map_err(|err| Error::Decode(format!("Invalid bytes slice: {:?}", err).into())) -} - -fn parse_float_from_str(s: &str, error_msg: &str) -> Result { - s.parse().map_err(|_| Error::Decode(error_msg.into())) -} - #[cfg(test)] mod box_tests { use std::str::FromStr; - use super::{pg_box_from_bytes, PgBox}; + use super::PgBox; const BOX_BYTES: &[u8] = &[ 63, 241, 153, 153, 153, 153, 153, 154, 64, 1, 153, 153, 153, 153, 153, 154, 64, 10, 102, @@ -134,7 +135,7 @@ mod box_tests { #[test] fn can_deserialise_box_type_bytes() { - let pg_box = pg_box_from_bytes(BOX_BYTES).unwrap(); + let pg_box = PgBox::from_bytes(BOX_BYTES).unwrap(); assert_eq!( pg_box, PgBox { From 0993ef0fa922fa913ea12e7dd4449f9ff08dd44e Mon Sep 17 00:00:00 2001 From: James Holman Date: Wed, 28 Aug 2024 21:18:27 +1000 Subject: [PATCH 10/64] fix: path --- sqlx-postgres/src/types/geometry/path.rs | 142 ++++++++++++++++------- 1 file changed, 102 insertions(+), 40 deletions(-) diff --git a/sqlx-postgres/src/types/geometry/path.rs b/sqlx-postgres/src/types/geometry/path.rs index 7424acd2d6..5c0d1163e9 100644 --- a/sqlx-postgres/src/types/geometry/path.rs +++ b/sqlx-postgres/src/types/geometry/path.rs @@ -3,6 +3,7 @@ use crate::encode::{Encode, IsNull}; use crate::error::BoxDynError; use crate::types::{PgPoint, Type}; use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, Postgres}; +use sqlx_core::bytes::Buf; use sqlx_core::Error; use std::str::FromStr; @@ -21,6 +22,12 @@ pub struct PgPath { pub points: Vec, } +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +struct Header { + is_closed: bool, + length: usize, +} + impl Type for PgPath { fn type_info() -> PgTypeInfo { PgTypeInfo::with_name("path") @@ -37,7 +44,7 @@ impl<'r> Decode<'r, Postgres> for PgPath { fn decode(value: PgValueRef<'r>) -> Result> { match value.format() { PgValueFormat::Text => Ok(PgPath::from_str(value.as_str()?)?), - PgValueFormat::Binary => Ok(pg_path_from_bytes(value.as_bytes()?)?), + PgValueFormat::Binary => Ok(PgPath::from_bytes(value.as_bytes()?)?), } } } @@ -81,28 +88,53 @@ impl FromStr for PgPath { } } -fn pg_path_from_bytes(bytes: &[u8]) -> Result { - let mut points = vec![]; +impl PgPath { + fn header(&self) -> Header { + Header { + is_closed: self.closed, + length: self.points.len(), + } + } - let offset: usize = 5; - let closed = get_i8_from_bytes(bytes, 0)? == 1i8; + fn from_bytes(mut bytes: &[u8]) -> Result { + let header = Header::try_read(&mut bytes).map_err(|s| Error::Decode(s.into()))?; - let steps = (bytes.len() - offset) / BYTE_WIDTH; - for n in (0..steps).step_by(2) { - let x = get_f64_from_bytes(bytes, BYTE_WIDTH * n + offset)?; - let y = get_f64_from_bytes(bytes, BYTE_WIDTH * (n + 1) + offset)?; - points.push(PgPoint { x, y }) - } + if bytes.len() != header.data_size() { + return Err(Error::Decode( + format!("error decoding PATH (length: {})", header.length).into(), + )); + } - Ok(PgPath { points, closed }) -} + if bytes.len() % BYTE_WIDTH * 2 != 0 { + return Err(Error::Decode( + format!( + "data length not divisible by pairs of {BYTE_WIDTH}: {}", + bytes.len() + ) + .into(), + )); + } + + let mut out_points = Vec::with_capacity(bytes.len() / BYTE_WIDTH * 2); + while bytes.has_remaining() { + let point = PgPoint { + x: bytes.get_f64(), + y: bytes.get_f64(), + }; + out_points.push(point) + } + Ok(PgPath { + closed: header.is_closed, + points: out_points, + }) + } -impl PgPath { fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), Error> { - let closed = self.closed as i8; - let length = self.points.len() as i32; - buff.extend_from_slice(&closed.to_be_bytes()); - buff.extend_from_slice(&length.to_be_bytes()); + let header = self.header(); + buff.reserve(header.data_size()); + header + .try_write(buff) + .map_err(|s| Error::Encode(s.into()))?; for point in &self.points { buff.extend_from_slice(&point.x.to_be_bytes()); @@ -119,26 +151,56 @@ impl PgPath { } } -fn get_i8_from_bytes(bytes: &[u8], start: usize) -> Result { - bytes - .get(start..start + 1) - .ok_or(Error::Decode( - format!("Could not decode path bytes: {:?}", bytes).into(), - ))? - .try_into() - .map(i8::from_be_bytes) - .map_err(|err| Error::Decode(format!("Invalid bytes slice: {:?}", err).into())) -} +impl Header { + const PACKED_WIDTH: usize = size_of::() + size_of::(); -fn get_f64_from_bytes(bytes: &[u8], start: usize) -> Result { - bytes - .get(start..start + BYTE_WIDTH) - .ok_or(Error::Decode( - format!("Could not decode path bytes: {:?}", bytes).into(), - ))? - .try_into() - .map(f64::from_be_bytes) - .map_err(|err| Error::Decode(format!("Invalid bytes slice: {:?}", err).into())) + fn data_size(&self) -> usize { + self.length * BYTE_WIDTH + } + + fn try_read(buf: &mut &[u8]) -> Result { + if buf.len() < Self::PACKED_WIDTH { + return Err(format!( + "expected PATH data to contain at least {} bytes, got {}", + Self::PACKED_WIDTH, + buf.len() + )); + } + + let is_closed = buf.get_i8(); + let length = buf.get_i32(); + + // can only overflow on 16-bit platforms + let length = usize::try_from(length).ok().ok_or_else(|| { + format!("received PATH data with greater than expected length: {length}") + })?; + + // can only overflow on 16-bit platforms + + Ok(Self { + is_closed: is_closed != 0, + length, + }) + } + + fn try_write(&self, buff: &mut PgArgumentBuffer) -> Result<(), String> { + let is_closed = self.is_closed as i8; + + let length = i32::try_from(self.length).map_err(|_| { + format!( + "PATH length exceeds allowed maximum ({} > {})", + self.length, + i32::MAX + ) + })?; + + // https://github.com/postgres/postgres/blob/e3ec9dc1bf4983fcedb6f43c71ea12ee26aefc7a/contrib/cube/cubedata.h#L18-L24 + + buff.extend(is_closed.to_be_bytes()); + buff.extend(length.to_be_bytes()); + + Ok(()) + } } fn parse_float_from_str(s: &str, error_msg: &str) -> Result { @@ -152,7 +214,7 @@ mod path_tests { use crate::types::PgPoint; - use super::{pg_path_from_bytes, PgPath}; + use super::PgPath; const PATH_CLOSED_BYTES: &[u8] = &[ 1, 0, 0, 0, 2, 63, 240, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 64, 8, 0, 0, 0, 0, 0, 0, @@ -166,7 +228,7 @@ mod path_tests { #[test] fn can_deserialise_path_type_bytes_closed() { - let path = pg_path_from_bytes(PATH_CLOSED_BYTES).unwrap(); + let path = PgPath::from_bytes(PATH_CLOSED_BYTES).unwrap(); assert_eq!( path, PgPath { @@ -178,7 +240,7 @@ mod path_tests { #[test] fn can_deserialise_path_type_bytes_open() { - let path = pg_path_from_bytes(PATH_OPEN_BYTES).unwrap(); + let path = PgPath::from_bytes(PATH_OPEN_BYTES).unwrap(); assert_eq!( path, PgPath { From 4e58b172d9fa6d410435ce6b1cd0b08be6381220 Mon Sep 17 00:00:00 2001 From: James Holman Date: Wed, 28 Aug 2024 21:21:23 +1000 Subject: [PATCH 11/64] fix: path header length --- sqlx-postgres/src/types/geometry/path.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/sqlx-postgres/src/types/geometry/path.rs b/sqlx-postgres/src/types/geometry/path.rs index 5c0d1163e9..d9b85cf9d1 100644 --- a/sqlx-postgres/src/types/geometry/path.rs +++ b/sqlx-postgres/src/types/geometry/path.rs @@ -101,7 +101,12 @@ impl PgPath { if bytes.len() != header.data_size() { return Err(Error::Decode( - format!("error decoding PATH (length: {})", header.length).into(), + format!( + "expected {} bytes after header, got {}", + header.data_size(), + bytes.len() + ) + .into(), )); } @@ -155,7 +160,7 @@ impl Header { const PACKED_WIDTH: usize = size_of::() + size_of::(); fn data_size(&self) -> usize { - self.length * BYTE_WIDTH + self.length * BYTE_WIDTH * 2 } fn try_read(buf: &mut &[u8]) -> Result { From ffe610347e15c8a0d657a5e1e2fe2b58a893eac0 Mon Sep 17 00:00:00 2001 From: James Holman Date: Wed, 28 Aug 2024 21:25:06 +1000 Subject: [PATCH 12/64] fix: line segment --- .../src/types/geometry/line_segment.rs | 22 +++++++++---------- sqlx-postgres/src/types/geometry/path.rs | 3 --- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/sqlx-postgres/src/types/geometry/line_segment.rs b/sqlx-postgres/src/types/geometry/line_segment.rs index f8b45710cc..0d499aff24 100644 --- a/sqlx-postgres/src/types/geometry/line_segment.rs +++ b/sqlx-postgres/src/types/geometry/line_segment.rs @@ -40,7 +40,7 @@ impl<'r> Decode<'r, Postgres> for PgLSeg { fn decode(value: PgValueRef<'r>) -> Result> { match value.format() { PgValueFormat::Text => Ok(PgLSeg::from_str(value.as_str()?)?), - PgValueFormat::Binary => Ok(pg_lseg_from_bytes(value.as_bytes()?)?), + PgValueFormat::Binary => Ok(PgLSeg::from_bytes(value.as_bytes()?)?), } } } @@ -95,16 +95,16 @@ impl FromStr for PgLSeg { } } -fn pg_lseg_from_bytes(mut bytes: &[u8]) -> Result { - let x1 = bytes.get_f64(); - let y1 = bytes.get_f64(); - let x2 = bytes.get_f64(); - let y2 = bytes.get_f64(); +impl PgLSeg { + fn from_bytes(mut bytes: &[u8]) -> Result { + let x1 = bytes.get_f64(); + let y1 = bytes.get_f64(); + let x2 = bytes.get_f64(); + let y2 = bytes.get_f64(); - Ok(PgLSeg { x1, y1, x2, y2 }) -} + Ok(PgLSeg { x1, y1, x2, y2 }) + } -impl PgLSeg { fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), Error> { buff.extend_from_slice(&self.x1.to_be_bytes()); buff.extend_from_slice(&self.y1.to_be_bytes()); @@ -126,7 +126,7 @@ mod lseg_tests { use std::str::FromStr; - use super::{pg_lseg_from_bytes, PgLSeg}; + use super::PgLSeg; const LINE_SEGMENT_BYTES: &[u8] = &[ 63, 241, 153, 153, 153, 153, 153, 154, 64, 1, 153, 153, 153, 153, 153, 154, 64, 10, 102, @@ -135,7 +135,7 @@ mod lseg_tests { #[test] fn can_deserialise_lseg_type_bytes() { - let lseg = pg_lseg_from_bytes(LINE_SEGMENT_BYTES).unwrap(); + let lseg = PgLSeg::from_bytes(LINE_SEGMENT_BYTES).unwrap(); assert_eq!( lseg, PgLSeg { diff --git a/sqlx-postgres/src/types/geometry/path.rs b/sqlx-postgres/src/types/geometry/path.rs index d9b85cf9d1..90071dd6d7 100644 --- a/sqlx-postgres/src/types/geometry/path.rs +++ b/sqlx-postgres/src/types/geometry/path.rs @@ -175,13 +175,10 @@ impl Header { let is_closed = buf.get_i8(); let length = buf.get_i32(); - // can only overflow on 16-bit platforms let length = usize::try_from(length).ok().ok_or_else(|| { format!("received PATH data with greater than expected length: {length}") })?; - // can only overflow on 16-bit platforms - Ok(Self { is_closed: is_closed != 0, length, From 11543971ac1959724b73d1e389502f262c1c7b09 Mon Sep 17 00:00:00 2001 From: James Holman Date: Wed, 28 Aug 2024 21:28:29 +1000 Subject: [PATCH 13/64] fix: split not just 4 for path --- sqlx-postgres/src/types/geometry/path.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlx-postgres/src/types/geometry/path.rs b/sqlx-postgres/src/types/geometry/path.rs index 90071dd6d7..5243678826 100644 --- a/sqlx-postgres/src/types/geometry/path.rs +++ b/sqlx-postgres/src/types/geometry/path.rs @@ -66,7 +66,7 @@ impl FromStr for PgPath { fn from_str(s: &str) -> Result { let closed = !s.contains("["); let sanitised = s.replace(&['(', ')', '[', ']', ' '][..], ""); - let mut parts = sanitised.splitn(4, ","); + let mut parts = sanitised.split(","); let mut points = vec![]; From 409a50b104ad383a16fb57900968c5abb364e5b1 Mon Sep 17 00:00:00 2001 From: James Holman Date: Thu, 29 Aug 2024 18:45:59 +1000 Subject: [PATCH 14/64] fix: test for boxes --- 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 60035eed27..1399c0f474 100644 --- a/tests/postgres/types.rs +++ b/tests/postgres/types.rs @@ -521,7 +521,7 @@ test_type!(lseg(Postgres, #[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] test_type!(_lseg>(Postgres, - "array[lseg('(1,2,3,4)'),lseg('[(1.1, 2.2), (3.3, 4.4)]')]" @= vec![sqlx::postgres::types::PgLSeg { x1: 1., y1: 2., x2: 3., y2: 4 }, sqlx::postgres::types::PgLSeg { x1: 1.1, y1: 2.2, x2: 3.3, y2: 4.4 }], + "array[lseg('(1,2,3,4)'),lseg('[(1.1, 2.2), (3.3, 4.4)]')]" @= vec![sqlx::postgres::types::PgLSeg { x1: 1., y1: 2., x2: 3., y2: 4. }, sqlx::postgres::types::PgLSeg { x1: 1.1, y1: 2.2, x2: 3.3, y2: 4.4 }], )); #[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] @@ -531,7 +531,7 @@ test_type!(box(Postgres, #[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] test_type!(_box>(Postgres, - "array[box('(1,2,3,4)'),box('[(1.1, 2.2), (3.3, 4.4)]')]" @= vec![sqlx::postgres::types::PgBox { x1: 1., y1: 2., x2: 3., y2: 4 }, sqlx::postgres::types::Pgbox { x1: 1.1, y1: 2.2, x2: 3.3, y2: 4.4 }], + "array[box('(1,2,3,4)'),box('[(1.1, 2.2), (3.3, 4.4)]')]" @= vec![sqlx::postgres::types::PgBox { x1: 1., y1: 2., x2: 3., y2: 4. }, sqlx::postgres::types::Pgbox { x1: 1.1, y1: 2.2, x2: 3.3, y2: 4.4 }], )); #[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] From a5c7c00c73dde9927acba32f82767683e0ef8f57 Mon Sep 17 00:00:00 2001 From: James Holman Date: Thu, 29 Aug 2024 18:48:09 +1000 Subject: [PATCH 15/64] fix: path link --- sqlx-postgres/src/types/geometry/path.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlx-postgres/src/types/geometry/path.rs b/sqlx-postgres/src/types/geometry/path.rs index 5243678826..111d2cb7d7 100644 --- a/sqlx-postgres/src/types/geometry/path.rs +++ b/sqlx-postgres/src/types/geometry/path.rs @@ -15,7 +15,7 @@ const BYTE_WIDTH: usize = 8; /// Description: Open path or Closed path (similar to polygon) /// Representation: ((x1,y1),(x2,y2)) /// -/// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-GEOMETRIC-PATH +/// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-GEOMETRIC-PATHS #[derive(Debug, Clone, PartialEq)] pub struct PgPath { pub closed: bool, From 91ecaa138cbd0580f95da9ccb5d9d14389bb2367 Mon Sep 17 00:00:00 2001 From: James Holman Date: Thu, 29 Aug 2024 20:06:42 +1000 Subject: [PATCH 16/64] fix: pg box name in tests --- sqlx-postgres/src/types/geometry/path.rs | 2 +- tests/postgres/types.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sqlx-postgres/src/types/geometry/path.rs b/sqlx-postgres/src/types/geometry/path.rs index 111d2cb7d7..d266c32b71 100644 --- a/sqlx-postgres/src/types/geometry/path.rs +++ b/sqlx-postgres/src/types/geometry/path.rs @@ -13,7 +13,7 @@ const BYTE_WIDTH: usize = 8; /// /// Storage size: 16+16n bytes /// Description: Open path or Closed path (similar to polygon) -/// Representation: ((x1,y1),(x2,y2)) +/// Representation: Open [(x1,y1),...], Closed ((x1,y1),...) /// /// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-GEOMETRIC-PATHS #[derive(Debug, Clone, PartialEq)] diff --git a/tests/postgres/types.rs b/tests/postgres/types.rs index 1399c0f474..63d3828342 100644 --- a/tests/postgres/types.rs +++ b/tests/postgres/types.rs @@ -531,7 +531,7 @@ test_type!(box(Postgres, #[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] test_type!(_box>(Postgres, - "array[box('(1,2,3,4)'),box('[(1.1, 2.2), (3.3, 4.4)]')]" @= vec![sqlx::postgres::types::PgBox { x1: 1., y1: 2., x2: 3., y2: 4. }, sqlx::postgres::types::Pgbox { x1: 1.1, y1: 2.2, x2: 3.3, y2: 4.4 }], + "array[box('(1,2,3,4)'),box('[(1.1, 2.2), (3.3, 4.4)]')]" @= vec![sqlx::postgres::types::PgBox { x1: 1., y1: 2., x2: 3., y2: 4. }, sqlx::postgres::types::PgBox { x1: 1.1, y1: 2.2, x2: 3.3, y2: 4.4 }], )); #[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] From 2494a003abbd1bcf5753fa2e145f590b59d981dd Mon Sep 17 00:00:00 2001 From: James Holman Date: Thu, 29 Aug 2024 20:22:18 +1000 Subject: [PATCH 17/64] fix: syntax for box tests --- 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 63d3828342..4f9aa7f3ba 100644 --- a/tests/postgres/types.rs +++ b/tests/postgres/types.rs @@ -531,7 +531,7 @@ test_type!(box(Postgres, #[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] test_type!(_box>(Postgres, - "array[box('(1,2,3,4)'),box('[(1.1, 2.2), (3.3, 4.4)]')]" @= vec![sqlx::postgres::types::PgBox { x1: 1., y1: 2., x2: 3., y2: 4. }, sqlx::postgres::types::PgBox { x1: 1.1, y1: 2.2, x2: 3.3, y2: 4.4 }], + "array[box('1,2,3,4'),box('((1.1, 2.2), (3.3, 4.4)]')]" @= vec![sqlx::postgres::types::PgBox { x1: 1., y1: 2., x2: 3., y2: 4. }, sqlx::postgres::types::PgBox { x1: 1.1, y1: 2.2, x2: 3.3, y2: 4.4 }], )); #[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] From 28b4a9934d791adfa4b0691c2f7699ef6679fa66 Mon Sep 17 00:00:00 2001 From: James Holman Date: Fri, 30 Aug 2024 19:36:14 +1000 Subject: [PATCH 18/64] fix: polygon tests --- sqlx-postgres/src/types/geometry/box.rs | 20 +- sqlx-postgres/src/types/geometry/mod.rs | 1 + sqlx-postgres/src/types/geometry/polygon.rs | 316 ++++++++++++++++++++ sqlx-postgres/src/types/mod.rs | 2 + 4 files changed, 329 insertions(+), 10 deletions(-) create mode 100644 sqlx-postgres/src/types/geometry/polygon.rs diff --git a/sqlx-postgres/src/types/geometry/box.rs b/sqlx-postgres/src/types/geometry/box.rs index 8b93af13ce..daab8040a6 100644 --- a/sqlx-postgres/src/types/geometry/box.rs +++ b/sqlx-postgres/src/types/geometry/box.rs @@ -129,8 +129,8 @@ mod box_tests { use super::PgBox; const BOX_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, 64, 17, 153, 153, 153, 153, 153, 154, + 64, 0, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 192, 0, 0, 0, 0, 0, 0, 0, 192, 0, 0, 0, + 0, 0, 0, 0, ]; #[test] @@ -139,10 +139,10 @@ mod box_tests { assert_eq!( pg_box, PgBox { - x1: 1.1, - y1: 2.2, - x2: 3.3, - y2: 4.4 + x1: -2., + y1: 2., + x2: 2., + y2: -2. } ) } @@ -219,10 +219,10 @@ mod box_tests { #[test] fn can_serialise_box_type() { let pg_box = PgBox { - x1: 1.1, - y1: 2.2, - x2: 3.3, - y2: 4.4, + x1: -2., + y1: 2., + x2: 2., + y2: -2., }; assert_eq!(pg_box.serialize_to_vec(), BOX_BYTES,) } diff --git a/sqlx-postgres/src/types/geometry/mod.rs b/sqlx-postgres/src/types/geometry/mod.rs index f67846fef2..1437d72c5c 100644 --- a/sqlx-postgres/src/types/geometry/mod.rs +++ b/sqlx-postgres/src/types/geometry/mod.rs @@ -3,3 +3,4 @@ pub mod line; pub mod line_segment; pub mod path; pub mod point; +pub mod polygon; diff --git a/sqlx-postgres/src/types/geometry/polygon.rs b/sqlx-postgres/src/types/geometry/polygon.rs new file mode 100644 index 0000000000..f0cbaa3670 --- /dev/null +++ b/sqlx-postgres/src/types/geometry/polygon.rs @@ -0,0 +1,316 @@ +use crate::decode::Decode; +use crate::encode::{Encode, IsNull}; +use crate::error::BoxDynError; +use crate::types::{PgPoint, Type}; +use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, Postgres}; +use sqlx_core::bytes::Buf; +use sqlx_core::Error; +use std::str::FromStr; + +const BYTE_WIDTH: usize = 8; + +/// Postgres Geometric Polygon type +/// +/// Storage size: 40+16n bytes +/// Description: Polygon (similar to closed polygon) +/// Representation: ((x1,y1),...) +/// +/// Seeh ttps://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-POLYGON +#[derive(Debug, Clone, PartialEq)] +pub struct PgPolygon { + pub points: Vec, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +struct Header { + length: usize, +} + +impl Type for PgPolygon { + fn type_info() -> PgTypeInfo { + PgTypeInfo::with_name("polygon") + } +} + +impl PgHasArrayType for PgPolygon { + fn array_type_info() -> PgTypeInfo { + PgTypeInfo::with_name("_polygon") + } +} + +impl<'r> Decode<'r, Postgres> for PgPolygon { + fn decode(value: PgValueRef<'r>) -> Result> { + match value.format() { + PgValueFormat::Text => Ok(PgPolygon::from_str(value.as_str()?)?), + PgValueFormat::Binary => Ok(PgPolygon::from_bytes(value.as_bytes()?)?), + } + } +} + +impl<'q> Encode<'q, Postgres> for PgPolygon { + fn produces(&self) -> Option { + Some(PgTypeInfo::with_name("polygon")) + } + + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { + self.serialize(buf)?; + Ok(IsNull::No) + } +} + +impl FromStr for PgPolygon { + type Err = Error; + + fn from_str(s: &str) -> Result { + let sanitised = s.replace(&['(', ')', '[', ']', ' '][..], ""); + let mut parts = sanitised.split(","); + + let mut points = vec![]; + + while let (Some(x_str), Some(y_str)) = (parts.next(), parts.next()) { + let x = parse_float_from_str(x_str, "could not get x")?; + let y = parse_float_from_str(y_str, "could not get y")?; + + let point = PgPoint { x, y }; + points.push(point); + } + + if !points.is_empty() { + return Ok(PgPolygon { points }); + } + + Err(Error::Decode( + format!("could not get polygon from {}", s).into(), + )) + } +} + +impl PgPolygon { + fn header(&self) -> Header { + Header { + length: self.points.len(), + } + } + + fn from_bytes(mut bytes: &[u8]) -> Result { + let header = Header::try_read(&mut bytes).map_err(|s| Error::Decode(s.into()))?; + + if bytes.len() != header.data_size() { + return Err(Error::Decode( + format!( + "expected {} bytes after header, got {}", + header.data_size(), + bytes.len() + ) + .into(), + )); + } + + if bytes.len() % BYTE_WIDTH * 2 != 0 { + return Err(Error::Decode( + format!( + "data length not divisible by pairs of {BYTE_WIDTH}: {}", + bytes.len() + ) + .into(), + )); + } + + let mut out_points = Vec::with_capacity(bytes.len() / BYTE_WIDTH * 2); + while bytes.has_remaining() { + let point = PgPoint { + x: bytes.get_f64(), + y: bytes.get_f64(), + }; + out_points.push(point) + } + Ok(PgPolygon { points: out_points }) + } + + fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), Error> { + let header = self.header(); + buff.reserve(header.data_size()); + header + .try_write(buff) + .map_err(|s| Error::Encode(s.into()))?; + + for point in &self.points { + buff.extend_from_slice(&point.x.to_be_bytes()); + buff.extend_from_slice(&point.y.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() + } +} + +impl Header { + const PACKED_WIDTH: usize = size_of::() + size_of::(); + + fn data_size(&self) -> usize { + self.length * BYTE_WIDTH * 2 + } + + fn try_read(buf: &mut &[u8]) -> Result { + if buf.len() < Self::PACKED_WIDTH { + return Err(format!( + "expected polygon data to contain at least {} bytes, got {}", + Self::PACKED_WIDTH, + buf.len() + )); + } + + let length = buf.get_i32(); + + let length = usize::try_from(length).ok().ok_or_else(|| { + format!("received polygon data with greater than expected length: {length}") + })?; + + Ok(Self { length }) + } + + fn try_write(&self, buff: &mut PgArgumentBuffer) -> Result<(), String> { + let length = i32::try_from(self.length).map_err(|_| { + format!( + "polygon length exceeds allowed maximum ({} > {})", + self.length, + i32::MAX + ) + })?; + + // https://github.com/postgres/postgres/blob/e3ec9dc1bf4983fcedb6f43c71ea12ee26aefc7a/contrib/cube/cubedata.h#L18-L24 + + buff.extend(length.to_be_bytes()); + + Ok(()) + } +} + +fn parse_float_from_str(s: &str, error_msg: &str) -> Result { + s.parse().map_err(|_| Error::Decode(error_msg.into())) +} + +#[cfg(test)] +mod polygon_tests { + + use std::str::FromStr; + + use crate::types::PgPoint; + + use super::PgPolygon; + + const POLYGON_BYTES: &[u8] = &[ + 0, 0, 0, 12, 192, 0, 0, 0, 0, 0, 0, 0, 192, 8, 0, 0, 0, 0, 0, 0, 191, 240, 0, 0, 0, 0, 0, + 0, 192, 8, 0, 0, 0, 0, 0, 0, 191, 240, 0, 0, 0, 0, 0, 0, 191, 240, 0, 0, 0, 0, 0, 0, 63, + 240, 0, 0, 0, 0, 0, 0, 63, 240, 0, 0, 0, 0, 0, 0, 63, 240, 0, 0, 0, 0, 0, 0, 64, 8, 0, 0, + 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 64, 8, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 192, + 8, 0, 0, 0, 0, 0, 0, 63, 240, 0, 0, 0, 0, 0, 0, 192, 8, 0, 0, 0, 0, 0, 0, 63, 240, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 191, 240, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 191, + 240, 0, 0, 0, 0, 0, 0, 192, 0, 0, 0, 0, 0, 0, 0, 192, 0, 0, 0, 0, 0, 0, 0, 192, 0, 0, 0, 0, + 0, 0, 0, + ]; + + #[test] + fn can_deserialise_polygon_type_bytes() { + let polygon = PgPolygon::from_bytes(POLYGON_BYTES).unwrap(); + assert_eq!( + polygon, + PgPolygon { + points: vec![ + PgPoint { x: -2., y: -3. }, + PgPoint { x: -1., y: -3. }, + PgPoint { x: -1., y: -1. }, + PgPoint { x: 1., y: 1. }, + PgPoint { x: 1., y: 3. }, + PgPoint { x: 2., y: 3. }, + PgPoint { x: 2., y: -3. }, + PgPoint { x: 1., y: -3. }, + PgPoint { x: 1., y: 0. }, + PgPoint { x: -1., y: 0. }, + PgPoint { x: -1., y: -2. }, + PgPoint { x: -2., y: -2. } + ] + } + ) + } + + #[test] + fn can_deserialise_polygon_type_str_first_syntax() { + let polygon = PgPolygon::from_str("[( 1, 2), (3, 4 )]").unwrap(); + assert_eq!( + polygon, + PgPolygon { + points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }] + } + ); + } + #[test] + fn can_deserialise_polygon_type_str_second_syntax() { + let polygon = PgPolygon::from_str("(( 1, 2), (3, 4 ))").unwrap(); + assert_eq!( + polygon, + PgPolygon { + points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }] + } + ); + } + + #[test] + fn can_deserialise_polygon_type_str_third_syntax() { + let polygon = PgPolygon::from_str("(1, 2), (3, 4 )").unwrap(); + assert_eq!( + polygon, + PgPolygon { + points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }] + } + ); + } + + #[test] + fn can_deserialise_polygon_type_str_fourth_syntax() { + let polygon = PgPolygon::from_str("1, 2, 3, 4").unwrap(); + assert_eq!( + polygon, + PgPolygon { + points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }] + } + ); + } + + #[test] + fn can_deserialise_polygon_type_str_float() { + let polygon = PgPolygon::from_str("(1.1, 2.2), (3.3, 4.4)").unwrap(); + assert_eq!( + polygon, + PgPolygon { + points: vec![PgPoint { x: 1.1, y: 2.2 }, PgPoint { x: 3.3, y: 4.4 }] + } + ); + } + + #[test] + fn can_serialise_polygon_type() { + let polygon = PgPolygon { + points: vec![ + PgPoint { x: -2., y: -3. }, + PgPoint { x: -1., y: -3. }, + PgPoint { x: -1., y: -1. }, + PgPoint { x: 1., y: 1. }, + PgPoint { x: 1., y: 3. }, + PgPoint { x: 2., y: 3. }, + PgPoint { x: 2., y: -3. }, + PgPoint { x: 1., y: -3. }, + PgPoint { x: 1., y: 0. }, + PgPoint { x: -1., y: 0. }, + PgPoint { x: -1., y: -2. }, + PgPoint { x: -2., y: -2. }, + ], + }; + assert_eq!(polygon.serialize_to_vec(), POLYGON_BYTES,) + } +} diff --git a/sqlx-postgres/src/types/mod.rs b/sqlx-postgres/src/types/mod.rs index 9afb5bca44..91f6e6204b 100644 --- a/sqlx-postgres/src/types/mod.rs +++ b/sqlx-postgres/src/types/mod.rs @@ -26,6 +26,7 @@ //! | [`PgLSeg`] | LSEG | //! | [`PgBox`] | BOX | //! | [`PgPath`] | PATH | +//! | [`PgPath`] | PATH | //! | [`PgHstore`] | HSTORE | //! //! 1 SQLx generally considers `CITEXT` to be compatible with `String`, `&str`, etc., @@ -253,6 +254,7 @@ pub use geometry::line::PgLine; pub use geometry::line_segment::PgLSeg; pub use geometry::path::PgPath; pub use geometry::point::PgPoint; +pub use geometry::polygon::PgPolygon; pub use geometry::r#box::PgBox; pub use hstore::PgHstore; pub use interval::PgInterval; From d67ddf52384b0ab33d3939c4cffac44bc3ec27cf Mon Sep 17 00:00:00 2001 From: James Holman Date: Fri, 30 Aug 2024 19:57:14 +1000 Subject: [PATCH 19/64] fix: tests for paths and integration tests for polygons --- sqlx-postgres/src/type_checking.rs | 2 ++ sqlx-postgres/src/types/geometry/box.rs | 38 +++++++++++++++++++------ tests/postgres/types.rs | 10 +++++++ 3 files changed, 41 insertions(+), 9 deletions(-) diff --git a/sqlx-postgres/src/type_checking.rs b/sqlx-postgres/src/type_checking.rs index 6258aca4ae..d6f57bfc9f 100644 --- a/sqlx-postgres/src/type_checking.rs +++ b/sqlx-postgres/src/type_checking.rs @@ -42,6 +42,8 @@ impl_type_checking!( sqlx::postgres::types::PgPath, + sqlx::postgres::types::PgPolygon, + #[cfg(feature = "uuid")] sqlx::types::Uuid, diff --git a/sqlx-postgres/src/types/geometry/box.rs b/sqlx-postgres/src/types/geometry/box.rs index daab8040a6..c99c1fdca4 100644 --- a/sqlx-postgres/src/types/geometry/box.rs +++ b/sqlx-postgres/src/types/geometry/box.rs @@ -105,11 +105,20 @@ impl PgBox { Ok(PgBox { x1, y1, x2, y2 }) } + /// > Any two opposite corners can be supplied on input, but the values will be reordered as needed to store the upper right and lower left corners, in that order. + /// + /// see: https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-GEOMETRIC-BOXES fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), Error> { - buff.extend_from_slice(&self.x1.to_be_bytes()); - buff.extend_from_slice(&self.y1.to_be_bytes()); - buff.extend_from_slice(&self.x2.to_be_bytes()); - buff.extend_from_slice(&self.y2.to_be_bytes()); + let min_x = &self.x1.min(self.x2); + let min_y = &self.y1.min(self.y2); + let max_x = &self.x1.max(self.x2); + let max_y = &self.y1.max(self.y2); + + buff.extend_from_slice(&max_x.to_be_bytes()); + buff.extend_from_slice(&max_y.to_be_bytes()); + buff.extend_from_slice(&min_x.to_be_bytes()); + buff.extend_from_slice(&min_y.to_be_bytes()); + Ok(()) } @@ -134,14 +143,14 @@ mod box_tests { ]; #[test] - fn can_deserialise_box_type_bytes() { + fn can_deserialise_box_type_bytes_in_order() { let pg_box = PgBox::from_bytes(BOX_BYTES).unwrap(); assert_eq!( pg_box, PgBox { - x1: -2., + x1: 2., y1: 2., - x2: 2., + x2: -2., y2: -2. } ) @@ -217,11 +226,22 @@ mod box_tests { } #[test] - fn can_serialise_box_type() { + fn can_serialise_box_type_in_order() { + let pg_box = PgBox { + x1: 2., + x2: -2., + y1: -2., + y2: 2., + }; + assert_eq!(pg_box.serialize_to_vec(), BOX_BYTES,) + } + + #[test] + fn can_serialise_box_type_out_of_order() { let pg_box = PgBox { x1: -2., - y1: 2., x2: 2., + y1: 2., y2: -2., }; assert_eq!(pg_box.serialize_to_vec(), BOX_BYTES,) diff --git a/tests/postgres/types.rs b/tests/postgres/types.rs index 4f9aa7f3ba..f47c2a7c1a 100644 --- a/tests/postgres/types.rs +++ b/tests/postgres/types.rs @@ -545,6 +545,16 @@ test_type!(_path>(Postgres, "array[path('(1,2),(3,4)'),path('[(1.1, 2.2), (3.3, 4.4)]')]" @= vec![sqlx::postgres::types::PgPath { closed: true, points: vec![ sqlx::postgres::types::PgPoint { x: 1., y: 2. }, sqlx::postgres::types::PgPoint { x: 3. , y: 4. } ]}, sqlx::postgres::types::PgPath { closed: false, points: vec![ sqlx::postgres::types::PgPoint { x: 1.1, y: 2.2 }, sqlx::postgres::types::PgPoint { x: 3.3 , y: 4.4 } ]},], )); +#[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] +test_type!(polygon(Postgres, + "polygon('((-2,-3),(-1,-3),(-1,-1),(1,1),(1,3),(2,3),(2,-3),(1,-3),(1,0),(-1,0),(-1,-2),(-2,-2))')" @= sqlx::postgres::types::PgPath { closed: true, points: vec![ + PgPoint { x: -2., y: -3. }, PgPoint { x: -1., y: -3. }, PgPoint { x: -1., y: -1. }, PgPoint { x: 1., y: 1. }, + PgPoint { x: 1., y: 3. }, PgPoint { x: 2., y: 3. }, PgPoint { x: 2., y: -3. }, PgPoint { x: 1., y: -3. }, + PgPoint { x: 1., y: 0. }, PgPoint { x: -1., y: 0. }, PgPoint { x: -1., y: -2. }, PgPoint { x: -2., y: -2. }, + + ]}, +)); + #[cfg(feature = "rust_decimal")] test_type!(decimal(Postgres, "0::numeric" == sqlx::types::Decimal::from_str("0").unwrap(), From a9b0d578890cf33ec170f8c437bfa1b2c8695f65 Mon Sep 17 00:00:00 2001 From: James Holman Date: Fri, 30 Aug 2024 20:06:40 +1000 Subject: [PATCH 20/64] docs: copy in some postgres docs --- sqlx-postgres/src/types/geometry/box.rs | 13 ++++++++++++- sqlx-postgres/src/types/geometry/line.rs | 6 ++++-- sqlx-postgres/src/types/geometry/line_segment.rs | 15 ++++++++++++++- sqlx-postgres/src/types/geometry/path.rs | 16 +++++++++++++++- sqlx-postgres/src/types/geometry/point.rs | 9 ++++++++- sqlx-postgres/src/types/geometry/polygon.rs | 16 +++++++++++++++- 6 files changed, 68 insertions(+), 7 deletions(-) diff --git a/sqlx-postgres/src/types/geometry/box.rs b/sqlx-postgres/src/types/geometry/box.rs index c99c1fdca4..af6767e598 100644 --- a/sqlx-postgres/src/types/geometry/box.rs +++ b/sqlx-postgres/src/types/geometry/box.rs @@ -13,7 +13,18 @@ const ERROR: &str = "error decoding BOX"; /// /// Storage size: 32 bytes /// Description: Rectangular box -/// Representation: ((x1,y1),(x2,y2)) +/// Representation: `((x1,y1),(x2,y2))` +/// +/// Boxes are represented by pairs of points that are opposite corners of the box. Values of type box are specified using any of the following syntaxes: +/// +/// ``` +/// ( ( x1 , y1 ) , ( x2 , y2 ) ) +/// ( x1 , y1 ) , ( x2 , y2 ) +/// x1 , y1 , x2 , y2 +/// ``` +/// where `(x1,y1) and (x2,y2)` are any two opposite corners of the box. +/// Boxes are output using the second syntax. +/// Any two opposite corners can be supplied on input, but the values will be reordered as needed to store the upper right and lower left corners, in that order. /// /// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-GEOMETRIC-BOXES #[derive(Debug, Clone, PartialEq)] diff --git a/sqlx-postgres/src/types/geometry/line.rs b/sqlx-postgres/src/types/geometry/line.rs index b1b1a81210..0e3ac23e76 100644 --- a/sqlx-postgres/src/types/geometry/line.rs +++ b/sqlx-postgres/src/types/geometry/line.rs @@ -9,11 +9,13 @@ use std::str::FromStr; const ERROR: &str = "error decoding LINE"; -/// Postgres Geometric Line type +/// ## Postgres Geometric Line type /// /// Storage size: 24 bytes /// Description: Infinite line -/// Representation: {A, B, C} +/// Representation: `{A, B, C}` +/// +/// Lines are represented by the linear equation Ax + By + C = 0, where A and B are not both zero. Values of type line are input and output in the following form: /// /// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-LINE #[derive(Debug, Clone, PartialEq)] diff --git a/sqlx-postgres/src/types/geometry/line_segment.rs b/sqlx-postgres/src/types/geometry/line_segment.rs index 0d499aff24..0318fe9d79 100644 --- a/sqlx-postgres/src/types/geometry/line_segment.rs +++ b/sqlx-postgres/src/types/geometry/line_segment.rs @@ -13,7 +13,20 @@ const ERROR: &str = "error decoding LSEG"; /// /// Storage size: 32 bytes /// Description: Finite line segment -/// Representation: ((x1,y1),(x2,y2)) +/// Representation: `((x1,y1),(x2,y2))` +/// +/// +/// Line segments are represented by pairs of points that are the endpoints of the segment. Values of type lseg are specified using any of the following syntaxes: +/// ``` +/// [ ( x1 , y1 ) , ( x2 , y2 ) ] +/// ( ( x1 , y1 ) , ( x2 , y2 ) ) +/// ( x1 , y1 ) , ( x2 , y2 ) +/// x1 , y1 , x2 , y2 +/// ``` +/// where `(x1,y1) and (x2,y2)` are the end points of the line segment. +/// +/// Line segments are output using the first syntax. +/// /// /// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-LSEG #[derive(Debug, Clone, PartialEq)] diff --git a/sqlx-postgres/src/types/geometry/path.rs b/sqlx-postgres/src/types/geometry/path.rs index d266c32b71..061eba9b00 100644 --- a/sqlx-postgres/src/types/geometry/path.rs +++ b/sqlx-postgres/src/types/geometry/path.rs @@ -13,7 +13,21 @@ const BYTE_WIDTH: usize = 8; /// /// Storage size: 16+16n bytes /// Description: Open path or Closed path (similar to polygon) -/// Representation: Open [(x1,y1),...], Closed ((x1,y1),...) +/// Representation: Open `[(x1,y1),...]`, Closed `((x1,y1),...)` +/// +/// Paths are represented by lists of connected points. Paths can be open, where the first and last points in the list are considered not connected, or closed, where the first and last points are considered connected. +/// Values of type path are specified using any of the following syntaxes: +/// ``` +/// [ ( x1 , y1 ) , ... , ( xn , yn ) ] +/// ( ( x1 , y1 ) , ... , ( xn , yn ) ) +/// ( x1 , y1 ) , ... , ( xn , yn ) +/// ( x1 , y1 , ... , xn , yn ) +/// x1 , y1 , ... , xn , yn +/// ``` +/// where the points are the end points of the line segments comprising the path. Square brackets `([])` indicate an open path, while parentheses `(())` indicate a closed path. +/// When the outermost parentheses are omitted, as in the third through fifth syntaxes, a closed path is assumed. +/// +/// Paths are output using the first or second syntax, as appropriate. /// /// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-GEOMETRIC-PATHS #[derive(Debug, Clone, PartialEq)] diff --git a/sqlx-postgres/src/types/geometry/point.rs b/sqlx-postgres/src/types/geometry/point.rs index a60bb39bd4..38d0820aaf 100644 --- a/sqlx-postgres/src/types/geometry/point.rs +++ b/sqlx-postgres/src/types/geometry/point.rs @@ -11,7 +11,14 @@ use std::str::FromStr; /// /// Storage size: 16 bytes /// Description: Point on a plane -/// Representation: (x, y) +/// 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: +/// ``` +/// ( 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)] diff --git a/sqlx-postgres/src/types/geometry/polygon.rs b/sqlx-postgres/src/types/geometry/polygon.rs index f0cbaa3670..0cff86d046 100644 --- a/sqlx-postgres/src/types/geometry/polygon.rs +++ b/sqlx-postgres/src/types/geometry/polygon.rs @@ -13,7 +13,21 @@ const BYTE_WIDTH: usize = 8; /// /// Storage size: 40+16n bytes /// Description: Polygon (similar to closed polygon) -/// Representation: ((x1,y1),...) +/// Representation: `((x1,y1),...)` +/// +/// Polygons are represented by lists of points (the vertexes of the polygon). Polygons are very similar to closed paths; the essential semantic difference is that a polygon is considered to include the area within it, while a path is not. +/// An important implementation difference between polygons and paths is that the stored representation of a polygon includes its smallest bounding box. This speeds up certain search operations, although computing the bounding box adds overhead while constructing new polygons. +/// Values of type polygon are specified using any of the following syntaxes: +/// +/// ``` +/// ( ( x1 , y1 ) , ... , ( xn , yn ) ) +/// ( x1 , y1 ) , ... , ( xn , yn ) +/// ( x1 , y1 , ... , xn , yn ) +/// x1 , y1 , ... , xn , yn +/// ``` +/// +/// where the points are the end points of the line segments comprising the boundary of the polygon. +/// Polygons are output using the first syntax. /// /// Seeh ttps://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-POLYGON #[derive(Debug, Clone, PartialEq)] From 4315174ebd57baa203681585548b6c4f8de9f8a0 Mon Sep 17 00:00:00 2001 From: James Holman Date: Fri, 30 Aug 2024 20:07:01 +1000 Subject: [PATCH 21/64] docs: remove duplicate docs --- sqlx-postgres/src/types/geometry/box.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/sqlx-postgres/src/types/geometry/box.rs b/sqlx-postgres/src/types/geometry/box.rs index af6767e598..22c356e81f 100644 --- a/sqlx-postgres/src/types/geometry/box.rs +++ b/sqlx-postgres/src/types/geometry/box.rs @@ -116,9 +116,6 @@ impl PgBox { Ok(PgBox { x1, y1, x2, y2 }) } - /// > Any two opposite corners can be supplied on input, but the values will be reordered as needed to store the upper right and lower left corners, in that order. - /// - /// see: https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-GEOMETRIC-BOXES fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), Error> { let min_x = &self.x1.min(self.x2); let min_y = &self.y1.min(self.y2); From 6e535e9413a4265756588b26ea32b845e0497286 Mon Sep 17 00:00:00 2001 From: James Holman Date: Fri, 30 Aug 2024 20:08:23 +1000 Subject: [PATCH 22/64] fix: polygon in docs --- sqlx-postgres/src/types/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlx-postgres/src/types/mod.rs b/sqlx-postgres/src/types/mod.rs index 91f6e6204b..03621bbe73 100644 --- a/sqlx-postgres/src/types/mod.rs +++ b/sqlx-postgres/src/types/mod.rs @@ -26,7 +26,7 @@ //! | [`PgLSeg`] | LSEG | //! | [`PgBox`] | BOX | //! | [`PgPath`] | PATH | -//! | [`PgPath`] | PATH | +//! | [`PgPolygon`] | POLYGON | //! | [`PgHstore`] | HSTORE | //! //! 1 SQLx generally considers `CITEXT` to be compatible with `String`, `&str`, etc., From afb00b5482b34735948b96c8940a5e7f8940592c Mon Sep 17 00:00:00 2001 From: James Holman Date: Fri, 30 Aug 2024 20:08:55 +1000 Subject: [PATCH 23/64] docs: align md table --- sqlx-postgres/src/types/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sqlx-postgres/src/types/mod.rs b/sqlx-postgres/src/types/mod.rs index 03621bbe73..d61fce8c40 100644 --- a/sqlx-postgres/src/types/mod.rs +++ b/sqlx-postgres/src/types/mod.rs @@ -21,12 +21,12 @@ //! | [`PgLQuery`] | LQUERY | //! | [`PgCiText`] | CITEXT1 | //! | [`PgCube`] | CUBE | -//! | [`PgPoint] | POINT | +//! | [`PgPoint] | POINT | //! | [`PgLine`] | LINE | //! | [`PgLSeg`] | LSEG | -//! | [`PgBox`] | BOX | +//! | [`PgBox`] | BOX | //! | [`PgPath`] | PATH | -//! | [`PgPolygon`] | POLYGON | +//! | [`PgPolygon`] | POLYGON | //! | [`PgHstore`] | HSTORE | //! //! 1 SQLx generally considers `CITEXT` to be compatible with `String`, `&str`, etc., From c2f652f2347cb834e8bbfc79494eda53ab0ce631 Mon Sep 17 00:00:00 2001 From: James Holman Date: Fri, 30 Aug 2024 20:09:25 +1000 Subject: [PATCH 24/64] docs: remove unused comments --- sqlx-postgres/src/types/geometry/path.rs | 2 -- sqlx-postgres/src/types/geometry/polygon.rs | 2 -- 2 files changed, 4 deletions(-) diff --git a/sqlx-postgres/src/types/geometry/path.rs b/sqlx-postgres/src/types/geometry/path.rs index 061eba9b00..f75dab6792 100644 --- a/sqlx-postgres/src/types/geometry/path.rs +++ b/sqlx-postgres/src/types/geometry/path.rs @@ -210,8 +210,6 @@ impl Header { ) })?; - // https://github.com/postgres/postgres/blob/e3ec9dc1bf4983fcedb6f43c71ea12ee26aefc7a/contrib/cube/cubedata.h#L18-L24 - buff.extend(is_closed.to_be_bytes()); buff.extend(length.to_be_bytes()); diff --git a/sqlx-postgres/src/types/geometry/polygon.rs b/sqlx-postgres/src/types/geometry/polygon.rs index 0cff86d046..25c726c0ec 100644 --- a/sqlx-postgres/src/types/geometry/polygon.rs +++ b/sqlx-postgres/src/types/geometry/polygon.rs @@ -197,8 +197,6 @@ impl Header { ) })?; - // https://github.com/postgres/postgres/blob/e3ec9dc1bf4983fcedb6f43c71ea12ee26aefc7a/contrib/cube/cubedata.h#L18-L24 - buff.extend(length.to_be_bytes()); Ok(()) From 0a34651ba6db47bc89f38e9424024454abe4011d Mon Sep 17 00:00:00 2001 From: James Holman Date: Fri, 30 Aug 2024 20:15:01 +1000 Subject: [PATCH 25/64] test: make box ordering test explicit --- sqlx-postgres/src/types/geometry/box.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/sqlx-postgres/src/types/geometry/box.rs b/sqlx-postgres/src/types/geometry/box.rs index 22c356e81f..248438ba5f 100644 --- a/sqlx-postgres/src/types/geometry/box.rs +++ b/sqlx-postgres/src/types/geometry/box.rs @@ -254,4 +254,26 @@ mod box_tests { }; assert_eq!(pg_box.serialize_to_vec(), BOX_BYTES,) } + + #[test] + fn can_order_box() { + let pg_box = PgBox { + x1: -2., + x2: 2., + y1: 2., + y2: -2., + }; + let bytes = pg_box.serialize_to_vec(); + + let pg_box = PgBox::from_bytes(&bytes).unwrap(); + assert_eq!( + pg_box, + PgBox { + x1: 2., + y1: 2., + x2: -2., + y2: -2. + } + ) + } } From a1c8a64977dbcc841cda4c3fe8ec63407d02512b Mon Sep 17 00:00:00 2001 From: James Holman Date: Fri, 30 Aug 2024 20:25:52 +1000 Subject: [PATCH 26/64] feat: add circle --- sqlx-postgres/src/types/geometry/circle.rs | 183 +++++++++++++++++++++ sqlx-postgres/src/types/geometry/mod.rs | 1 + sqlx-postgres/src/types/mod.rs | 2 + 3 files changed, 186 insertions(+) create mode 100644 sqlx-postgres/src/types/geometry/circle.rs diff --git a/sqlx-postgres/src/types/geometry/circle.rs b/sqlx-postgres/src/types/geometry/circle.rs new file mode 100644 index 0000000000..b1fc2d4c1e --- /dev/null +++ b/sqlx-postgres/src/types/geometry/circle.rs @@ -0,0 +1,183 @@ +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; + +const ERROR: &str = "error decoding CIRCLE"; + +/// ## Postgres Geometric Circle type +/// +/// Storage size: 24 bytes +/// Description: Circle +/// Representation: `< (x, y), r >` (center point and radius) +/// +/// ``` +/// < ( x , y ) , r > +/// ( ( x , y ) , r ) +/// ( x , y ) , r +/// x , y , r +/// ``` +/// where `(x,y)` is the center point and r is the radius of the circle. +/// +/// Circles are output using the first syntax. +/// +/// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-CIRCLE +#[derive(Debug, Clone, PartialEq)] +pub struct PgCircle { + pub x: f64, + pub y: f64, + pub r: f64, +} + +impl Type for PgCircle { + fn type_info() -> PgTypeInfo { + PgTypeInfo::with_name("circle") + } +} + +impl PgHasArrayType for PgCircle { + fn array_type_info() -> PgTypeInfo { + PgTypeInfo::with_name("_circle") + } +} + +impl<'r> Decode<'r, Postgres> for PgCircle { + fn decode(value: PgValueRef<'r>) -> Result> { + match value.format() { + PgValueFormat::Text => Ok(PgCircle::from_str(value.as_str()?)?), + PgValueFormat::Binary => Ok(PgCircle::from_bytes(value.as_bytes()?)?), + } + } +} + +impl<'q> Encode<'q, Postgres> for PgCircle { + fn produces(&self) -> Option { + Some(PgTypeInfo::with_name("circle")) + } + + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { + self.serialize(buf)?; + Ok(IsNull::No) + } +} + +impl FromStr for PgCircle { + type Err = Error; + + fn from_str(s: &str) -> Result { + let sanitised = s.replace(&['<', '>', '(', ')', ' '][..], ""); + let mut parts = sanitised.splitn(3, ','); + + let x = parts + .next() + .and_then(|s| s.trim().parse::().ok()) + .ok_or(Error::Decode( + format!("{}: could not get x from {}", ERROR, s).into(), + ))?; + + let y = parts + .next() + .and_then(|s| s.trim().parse::().ok()) + .ok_or(Error::Decode( + format!("{}: could not get y from {}", ERROR, s).into(), + ))?; + + let r = parts + .next() + .and_then(|s| s.trim().parse::().ok()) + .ok_or(Error::Decode( + format!("{}: could not get r from {}", ERROR, s).into(), + ))?; + + Ok(PgCircle { x, y, r }) + } +} + +impl PgCircle { + fn from_bytes(mut bytes: &[u8]) -> Result { + let x = bytes.get_f64(); + let b = bytes.get_f64(); + let r = bytes.get_f64(); + Ok(PgCircle { x, y: b, r }) + } + + fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), Error> { + buff.extend_from_slice(&self.x.to_be_bytes()); + buff.extend_from_slice(&self.y.to_be_bytes()); + buff.extend_from_slice(&self.r.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 circle_tests { + + use std::str::FromStr; + + use super::PgCircle; + + const CIRCLE_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_circle_type_bytes() { + let circle = PgCircle::from_bytes(CIRCLE_BYTES).unwrap(); + assert_eq!( + circle, + PgCircle { + x: 1.1, + y: 2.2, + r: 3.3 + } + ) + } + + #[test] + fn can_deserialise_circle_type_str() { + let circle = PgCircle::from_str("<(1, 2), 3 >").unwrap(); + assert_eq!( + circle, + PgCircle { + x: 1.0, + y: 2.0, + r: 3.0 + } + ); + } + + #[test] + fn can_deserialise_circle_type_str_float() { + let circle = PgCircle::from_str("<(1.1, 2.2), 3.3>").unwrap(); + assert_eq!( + circle, + PgCircle { + x: 1.1, + y: 2.2, + r: 3.3 + } + ); + } + + #[test] + fn can_serialise_circle_type() { + let circle = PgCircle { + x: 1.1, + y: 2.2, + r: 3.3, + }; + assert_eq!(circle.serialize_to_vec(), CIRCLE_BYTES,) + } +} diff --git a/sqlx-postgres/src/types/geometry/mod.rs b/sqlx-postgres/src/types/geometry/mod.rs index 1437d72c5c..c3142145ee 100644 --- a/sqlx-postgres/src/types/geometry/mod.rs +++ b/sqlx-postgres/src/types/geometry/mod.rs @@ -1,4 +1,5 @@ pub mod r#box; +pub mod circle; pub mod line; pub mod line_segment; pub mod path; diff --git a/sqlx-postgres/src/types/mod.rs b/sqlx-postgres/src/types/mod.rs index d61fce8c40..7dd25cb272 100644 --- a/sqlx-postgres/src/types/mod.rs +++ b/sqlx-postgres/src/types/mod.rs @@ -27,6 +27,7 @@ //! | [`PgBox`] | BOX | //! | [`PgPath`] | PATH | //! | [`PgPolygon`] | POLYGON | +//! | [`PgCircle`] | CIRCLE | //! | [`PgHstore`] | HSTORE | //! //! 1 SQLx generally considers `CITEXT` to be compatible with `String`, `&str`, etc., @@ -250,6 +251,7 @@ mod bit_vec; pub use array::PgHasArrayType; pub use citext::PgCiText; pub use cube::PgCube; +pub use geometry::circle::PgCircle; pub use geometry::line::PgLine; pub use geometry::line_segment::PgLSeg; pub use geometry::path::PgPath; From b9ea48927f84fb76760ba4d776739094a6ad7f3a Mon Sep 17 00:00:00 2001 From: James Holman Date: Fri, 30 Aug 2024 20:29:40 +1000 Subject: [PATCH 27/64] test: circle tests --- sqlx-postgres/src/type_checking.rs | 2 ++ sqlx-postgres/src/types/geometry/circle.rs | 39 ++++++++++++++++++++++ tests/postgres/types.rs | 14 +++++++- 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/sqlx-postgres/src/type_checking.rs b/sqlx-postgres/src/type_checking.rs index d6f57bfc9f..c4bfe5db48 100644 --- a/sqlx-postgres/src/type_checking.rs +++ b/sqlx-postgres/src/type_checking.rs @@ -44,6 +44,8 @@ impl_type_checking!( sqlx::postgres::types::PgPolygon, + sqlx::postgres::types::PgCircle, + #[cfg(feature = "uuid")] sqlx::types::Uuid, diff --git a/sqlx-postgres/src/types/geometry/circle.rs b/sqlx-postgres/src/types/geometry/circle.rs index b1fc2d4c1e..5d92ea0272 100644 --- a/sqlx-postgres/src/types/geometry/circle.rs +++ b/sqlx-postgres/src/types/geometry/circle.rs @@ -158,6 +158,45 @@ mod circle_tests { ); } + #[test] + fn can_deserialise_circle_type_str_second_syntax() { + let circle = PgCircle::from_str("((1, 2), 3 )").unwrap(); + assert_eq!( + circle, + PgCircle { + x: 1.0, + y: 2.0, + r: 3.0 + } + ); + } + + #[test] + fn can_deserialise_circle_type_str_third_syntax() { + let circle = PgCircle::from_str("(1, 2), 3 ").unwrap(); + assert_eq!( + circle, + PgCircle { + x: 1.0, + y: 2.0, + r: 3.0 + } + ); + } + + #[test] + fn can_deserialise_circle_type_str_fourth_syntax() { + let circle = PgCircle::from_str("1, 2) 3 ").unwrap(); + assert_eq!( + circle, + PgCircle { + x: 1.0, + y: 2.0, + r: 3.0 + } + ); + } + #[test] fn can_deserialise_circle_type_str_float() { let circle = PgCircle::from_str("<(1.1, 2.2), 3.3>").unwrap(); diff --git a/tests/postgres/types.rs b/tests/postgres/types.rs index f47c2a7c1a..b4e5022dfe 100644 --- a/tests/postgres/types.rs +++ b/tests/postgres/types.rs @@ -551,10 +551,22 @@ test_type!(polygon(Postgres, PgPoint { x: -2., y: -3. }, PgPoint { x: -1., y: -3. }, PgPoint { x: -1., y: -1. }, PgPoint { x: 1., y: 1. }, PgPoint { x: 1., y: 3. }, PgPoint { x: 2., y: 3. }, PgPoint { x: 2., y: -3. }, PgPoint { x: 1., y: -3. }, PgPoint { x: 1., y: 0. }, PgPoint { x: -1., y: 0. }, PgPoint { x: -1., y: -2. }, PgPoint { x: -2., y: -2. }, - ]}, )); +#[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] +test_type!(circle(Postgres, + "circle('<(1.1, -2.2), 3.3>')" @= sqlx::postgres::types::PgCircle { x: 1.1, y:-2.2, r: 3.3 }, + "circle('((1.1, -2.2), 3.3)')" @= sqlx::postgres::types::PgCircle { x: 1.1, y:-2.2, r: 3.3 }, + "circle('(1.1, -2.2), 3.3')" @= sqlx::postgres::types::PgCircle { x: 1.1, y:-2.2, r: 3.3 }, + "circle('1.1, -2.2, 3.3')" @= sqlx::postgres::types::PgCircle { x: 1.1, y:-2.2, r: 3.3 }, +)); + +#[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] +test_type!(_circle>(Postgres, + "array[circle('<(1,2,3}'),circle('{1.1, 2.2, 3.3}')]" @= vec![sqlx::postgres::types::PgCircle { a:1., b: 2., c: 3. }, sqlx::postgres::types::PgCircle { 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 5def29733e9947f59b0ece1ddc78b5eaf716b2da Mon Sep 17 00:00:00 2001 From: James Holman Date: Fri, 30 Aug 2024 20:32:04 +1000 Subject: [PATCH 28/64] test: import pgpoint to avoid repeat nesting --- tests/postgres/types.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/postgres/types.rs b/tests/postgres/types.rs index b4e5022dfe..97339da48d 100644 --- a/tests/postgres/types.rs +++ b/tests/postgres/types.rs @@ -3,7 +3,7 @@ extern crate time_ as time; use std::net::SocketAddr; use std::ops::Bound; -use sqlx::postgres::types::{Oid, PgCiText, PgInterval, PgMoney, PgRange}; +use sqlx::postgres::types::{Oid, PgCiText, PgInterval, PgMoney, PgPoint, PgRange}; use sqlx::postgres::Postgres; use sqlx_test::{new, test_decode_type, test_prepared_type, test_type}; @@ -536,18 +536,18 @@ test_type!(_box>(Postgres, #[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] test_type!(path(Postgres, - "path('((1.0, 2.0), (3.0,4.0))')" @= sqlx::postgres::types::PgPath { closed: true, points: vec![ sqlx::postgres::types::PgPoint { x: 1., y: 2. }, sqlx::postgres::types::PgPoint { x: 3. , y: 4. } ]}, - "path('[(1.0, 2.0), (3.0,4.0)]')" @= sqlx::postgres::types::PgPath { closed: false, points: vec![ sqlx::postgres::types::PgPoint { x: 1., y: 2. }, sqlx::postgres::types::PgPoint { x: 3. , y: 4. } ]}, + "path('((1.0, 2.0), (3.0,4.0))')" @= sqlx::postgres::types::PgPath { closed: true, points: vec![ PgPoint { x: 1., y: 2. }, PgPoint { x: 3. , y: 4. } ]}, + "path('[(1.0, 2.0), (3.0,4.0)]')" @= sqlx::postgres::types::PgPath { closed: false, points: vec![ PgPoint { x: 1., y: 2. }, PgPoint { x: 3. , y: 4. } ]}, )); #[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] test_type!(_path>(Postgres, - "array[path('(1,2),(3,4)'),path('[(1.1, 2.2), (3.3, 4.4)]')]" @= vec![sqlx::postgres::types::PgPath { closed: true, points: vec![ sqlx::postgres::types::PgPoint { x: 1., y: 2. }, sqlx::postgres::types::PgPoint { x: 3. , y: 4. } ]}, sqlx::postgres::types::PgPath { closed: false, points: vec![ sqlx::postgres::types::PgPoint { x: 1.1, y: 2.2 }, sqlx::postgres::types::PgPoint { x: 3.3 , y: 4.4 } ]},], + "array[path('(1,2),(3,4)'),path('[(1.1, 2.2), (3.3, 4.4)]')]" @= vec![sqlx::postgres::types::PgPath { closed: true, points: vec![ PgPoint { x: 1., y: 2. }, PgPoint { x: 3. , y: 4. } ]}, sqlx::postgres::types::PgPath { closed: false, points: vec![ PgPoint { x: 1.1, y: 2.2 }, PgPoint { x: 3.3 , y: 4.4 } ]},], )); #[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] test_type!(polygon(Postgres, - "polygon('((-2,-3),(-1,-3),(-1,-1),(1,1),(1,3),(2,3),(2,-3),(1,-3),(1,0),(-1,0),(-1,-2),(-2,-2))')" @= sqlx::postgres::types::PgPath { closed: true, points: vec![ + "polygon('((-2,-3),(-1,-3),(-1,-1),(1,1),(1,3),(2,3),(2,-3),(1,-3),(1,0),(-1,0),(-1,-2),(-2,-2))')" @= PgPath { closed: true, points: vec![ PgPoint { x: -2., y: -3. }, PgPoint { x: -1., y: -3. }, PgPoint { x: -1., y: -1. }, PgPoint { x: 1., y: 1. }, PgPoint { x: 1., y: 3. }, PgPoint { x: 2., y: 3. }, PgPoint { x: 2., y: -3. }, PgPoint { x: 1., y: -3. }, PgPoint { x: 1., y: 0. }, PgPoint { x: -1., y: 0. }, PgPoint { x: -1., y: -2. }, PgPoint { x: -2., y: -2. }, @@ -564,7 +564,7 @@ test_type!(circle(Postgres, #[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] test_type!(_circle>(Postgres, - "array[circle('<(1,2,3}'),circle('{1.1, 2.2, 3.3}')]" @= vec![sqlx::postgres::types::PgCircle { a:1., b: 2., c: 3. }, sqlx::postgres::types::PgCircle { a:1.1, b: 2.2, c: 3.3 }], + "array[circle('<(1,2,3}'),circle('{1.1, 2.2, 3.3}')]" @= vec![sqlx::postgres::types::PgCircle { x: 1., y: 2., r: 3. }, sqlx::postgres::types::PgCircle { x: 1.1, y: 2.2, r: 3.3 }], )); #[cfg(feature = "rust_decimal")] From 9be8dd6601af3ecc538d0c07c307f5a5a324cf72 Mon Sep 17 00:00:00 2001 From: James Holman Date: Fri, 30 Aug 2024 21:01:42 +1000 Subject: [PATCH 29/64] tests: polygon integ and circle unit --- sqlx-postgres/src/types/geometry/circle.rs | 2 +- tests/postgres/types.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sqlx-postgres/src/types/geometry/circle.rs b/sqlx-postgres/src/types/geometry/circle.rs index 5d92ea0272..06427177ea 100644 --- a/sqlx-postgres/src/types/geometry/circle.rs +++ b/sqlx-postgres/src/types/geometry/circle.rs @@ -186,7 +186,7 @@ mod circle_tests { #[test] fn can_deserialise_circle_type_str_fourth_syntax() { - let circle = PgCircle::from_str("1, 2) 3 ").unwrap(); + let circle = PgCircle::from_str("1, 2, 3 ").unwrap(); assert_eq!( circle, PgCircle { diff --git a/tests/postgres/types.rs b/tests/postgres/types.rs index 97339da48d..58b56a5cc7 100644 --- a/tests/postgres/types.rs +++ b/tests/postgres/types.rs @@ -547,10 +547,10 @@ test_type!(_path>(Postgres, #[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] test_type!(polygon(Postgres, - "polygon('((-2,-3),(-1,-3),(-1,-1),(1,1),(1,3),(2,3),(2,-3),(1,-3),(1,0),(-1,0),(-1,-2),(-2,-2))')" @= PgPath { closed: true, points: vec![ + "polygon('((-2,-3),(-1,-3),(-1,-1),(1,1),(1,3),(2,3),(2,-3),(1,-3),(1,0),(-1,0),(-1,-2),(-2,-2))')" @= sqlx::postgres::types::PgPolygon { points: vec![ PgPoint { x: -2., y: -3. }, PgPoint { x: -1., y: -3. }, PgPoint { x: -1., y: -1. }, PgPoint { x: 1., y: 1. }, - PgPoint { x: 1., y: 3. }, PgPoint { x: 2., y: 3. }, PgPoint { x: 2., y: -3. }, PgPoint { x: 1., y: -3. }, - PgPoint { x: 1., y: 0. }, PgPoint { x: -1., y: 0. }, PgPoint { x: -1., y: -2. }, PgPoint { x: -2., y: -2. }, + PgPoint { x: 1., y: 3. }, PgPoint { x: 2., y: 3. }, PgPoint { x: 2., y: -3. }, PgPoint { x: 1., y: -3. }, + PgPoint { x: 1., y: 0. }, PgPoint { x: -1., y: 0. }, PgPoint { x: -1., y: -2. }, PgPoint { x: -2., y: -2. }, ]}, )); From 1ddb3e99ec62485007e1ce6ae207d905bb2e66a8 Mon Sep 17 00:00:00 2001 From: "James H." <32926722+jayy-lmao@users.noreply.github.com> Date: Mon, 2 Sep 2024 19:32:15 +1000 Subject: [PATCH 30/64] fix: update sqlx-postgres/src/types/geometry/circle.rs Co-authored-by: Frank Elsinga --- sqlx-postgres/src/types/geometry/circle.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sqlx-postgres/src/types/geometry/circle.rs b/sqlx-postgres/src/types/geometry/circle.rs index 06427177ea..00ce5af3f6 100644 --- a/sqlx-postgres/src/types/geometry/circle.rs +++ b/sqlx-postgres/src/types/geometry/circle.rs @@ -101,8 +101,8 @@ impl PgCircle { fn from_bytes(mut bytes: &[u8]) -> Result { let x = bytes.get_f64(); let b = bytes.get_f64(); - let r = bytes.get_f64(); - Ok(PgCircle { x, y: b, r }) + let y = bytes.get_f64(); + Ok(PgCircle { x, y, r }) } fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), Error> { From 2ea7d167753151029da8bdc602ccc30cc5fce30c Mon Sep 17 00:00:00 2001 From: James Holman Date: Mon, 2 Sep 2024 19:44:46 +1000 Subject: [PATCH 31/64] fix: circle --- sqlx-postgres/src/types/geometry/box.rs | 9 ++++----- sqlx-postgres/src/types/geometry/circle.rs | 10 +++++++--- sqlx-postgres/src/types/geometry/line.rs | 1 - sqlx-postgres/src/types/geometry/line_segment.rs | 5 ++--- sqlx-postgres/src/types/geometry/path.rs | 5 ++--- sqlx-postgres/src/types/geometry/point.rs | 3 +-- sqlx-postgres/src/types/geometry/polygon.rs | 5 ++--- 7 files changed, 18 insertions(+), 20 deletions(-) diff --git a/sqlx-postgres/src/types/geometry/box.rs b/sqlx-postgres/src/types/geometry/box.rs index 248438ba5f..0d9bf768e6 100644 --- a/sqlx-postgres/src/types/geometry/box.rs +++ b/sqlx-postgres/src/types/geometry/box.rs @@ -11,13 +11,12 @@ const ERROR: &str = "error decoding BOX"; /// Postgres Geometric Box type /// -/// Storage size: 32 bytes /// Description: Rectangular box /// Representation: `((x1,y1),(x2,y2))` /// /// Boxes are represented by pairs of points that are opposite corners of the box. Values of type box are specified using any of the following syntaxes: /// -/// ``` +/// ```text /// ( ( x1 , y1 ) , ( x2 , y2 ) ) /// ( x1 , y1 ) , ( x2 , y2 ) /// x1 , y1 , x2 , y2 @@ -71,7 +70,7 @@ impl FromStr for PgBox { type Err = Error; fn from_str(s: &str) -> Result { - let sanitised = s.replace(&['(', ')', '[', ']', ' '][..], ""); + let sanitised = s.replace(['(', ')', '[', ']', ' '], ""); let mut parts = sanitised.splitn(4, ","); let x1 = parts @@ -107,7 +106,7 @@ impl FromStr for PgBox { } impl PgBox { - fn from_bytes(mut bytes: &[u8]) -> Result { + fn from_bytes(mut bytes: &[u8]) -> Result { let x1 = bytes.get_f64(); let y1 = bytes.get_f64(); let x2 = bytes.get_f64(); @@ -116,7 +115,7 @@ impl PgBox { Ok(PgBox { x1, y1, x2, y2 }) } - fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), Error> { + fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), String> { let min_x = &self.x1.min(self.x2); let min_y = &self.y1.min(self.y2); let max_x = &self.x1.max(self.x2); diff --git a/sqlx-postgres/src/types/geometry/circle.rs b/sqlx-postgres/src/types/geometry/circle.rs index 00ce5af3f6..815b874a64 100644 --- a/sqlx-postgres/src/types/geometry/circle.rs +++ b/sqlx-postgres/src/types/geometry/circle.rs @@ -11,11 +11,10 @@ const ERROR: &str = "error decoding CIRCLE"; /// ## Postgres Geometric Circle type /// -/// Storage size: 24 bytes /// Description: Circle /// Representation: `< (x, y), r >` (center point and radius) /// -/// ``` +/// ```text /// < ( x , y ) , r > /// ( ( x , y ) , r ) /// ( x , y ) , r @@ -69,7 +68,7 @@ impl FromStr for PgCircle { type Err = Error; fn from_str(s: &str) -> Result { - let sanitised = s.replace(&['<', '>', '(', ')', ' '][..], ""); + let sanitised = s.replace(['<', '>', '(', ')', ' '], ""); let mut parts = sanitised.splitn(3, ','); let x = parts @@ -100,8 +99,13 @@ impl FromStr for PgCircle { impl PgCircle { fn from_bytes(mut bytes: &[u8]) -> Result { let x = bytes.get_f64(); +<<<<<<< HEAD let b = bytes.get_f64(); let y = bytes.get_f64(); +======= + let y = bytes.get_f64(); + let r = bytes.get_f64(); +>>>>>>> 0c1caacd (fix: unit tests) Ok(PgCircle { x, y, r }) } diff --git a/sqlx-postgres/src/types/geometry/line.rs b/sqlx-postgres/src/types/geometry/line.rs index 0e3ac23e76..a883c570e0 100644 --- a/sqlx-postgres/src/types/geometry/line.rs +++ b/sqlx-postgres/src/types/geometry/line.rs @@ -11,7 +11,6 @@ const ERROR: &str = "error decoding LINE"; /// ## Postgres Geometric Line type /// -/// Storage size: 24 bytes /// Description: Infinite line /// Representation: `{A, B, C}` /// diff --git a/sqlx-postgres/src/types/geometry/line_segment.rs b/sqlx-postgres/src/types/geometry/line_segment.rs index 0318fe9d79..03e04b1ae9 100644 --- a/sqlx-postgres/src/types/geometry/line_segment.rs +++ b/sqlx-postgres/src/types/geometry/line_segment.rs @@ -11,13 +11,12 @@ const ERROR: &str = "error decoding LSEG"; /// Postgres Geometric Line Segment type /// -/// Storage size: 32 bytes /// Description: Finite line segment /// Representation: `((x1,y1),(x2,y2))` /// /// /// Line segments are represented by pairs of points that are the endpoints of the segment. Values of type lseg are specified using any of the following syntaxes: -/// ``` +/// ```text /// [ ( x1 , y1 ) , ( x2 , y2 ) ] /// ( ( x1 , y1 ) , ( x2 , y2 ) ) /// ( x1 , y1 ) , ( x2 , y2 ) @@ -73,7 +72,7 @@ impl FromStr for PgLSeg { type Err = Error; fn from_str(s: &str) -> Result { - let sanitised = s.replace(&['(', ')', '[', ']', ' '][..], ""); + let sanitised = s.replace(['(', ')', '[', ']', ' '], ""); let mut parts = sanitised.splitn(4, ","); let x1 = parts diff --git a/sqlx-postgres/src/types/geometry/path.rs b/sqlx-postgres/src/types/geometry/path.rs index f75dab6792..a07766d9fa 100644 --- a/sqlx-postgres/src/types/geometry/path.rs +++ b/sqlx-postgres/src/types/geometry/path.rs @@ -11,13 +11,12 @@ const BYTE_WIDTH: usize = 8; /// Postgres Geometric Path type /// -/// Storage size: 16+16n bytes /// Description: Open path or Closed path (similar to polygon) /// Representation: Open `[(x1,y1),...]`, Closed `((x1,y1),...)` /// /// Paths are represented by lists of connected points. Paths can be open, where the first and last points in the list are considered not connected, or closed, where the first and last points are considered connected. /// Values of type path are specified using any of the following syntaxes: -/// ``` +/// ```text /// [ ( x1 , y1 ) , ... , ( xn , yn ) ] /// ( ( x1 , y1 ) , ... , ( xn , yn ) ) /// ( x1 , y1 ) , ... , ( xn , yn ) @@ -79,7 +78,7 @@ impl FromStr for PgPath { fn from_str(s: &str) -> Result { let closed = !s.contains("["); - let sanitised = s.replace(&['(', ')', '[', ']', ' '][..], ""); + let sanitised = s.replace(['(', ')', '[', ']', ' '], ""); let mut parts = sanitised.split(","); let mut points = vec![]; diff --git a/sqlx-postgres/src/types/geometry/point.rs b/sqlx-postgres/src/types/geometry/point.rs index 38d0820aaf..f4ccedcbf7 100644 --- a/sqlx-postgres/src/types/geometry/point.rs +++ b/sqlx-postgres/src/types/geometry/point.rs @@ -9,12 +9,11 @@ use std::str::FromStr; /// Postgres Geometric Point type /// -/// Storage size: 16 bytes /// 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 /// ```` diff --git a/sqlx-postgres/src/types/geometry/polygon.rs b/sqlx-postgres/src/types/geometry/polygon.rs index 25c726c0ec..fa12bdfcab 100644 --- a/sqlx-postgres/src/types/geometry/polygon.rs +++ b/sqlx-postgres/src/types/geometry/polygon.rs @@ -11,7 +11,6 @@ const BYTE_WIDTH: usize = 8; /// Postgres Geometric Polygon type /// -/// Storage size: 40+16n bytes /// Description: Polygon (similar to closed polygon) /// Representation: `((x1,y1),...)` /// @@ -19,7 +18,7 @@ const BYTE_WIDTH: usize = 8; /// An important implementation difference between polygons and paths is that the stored representation of a polygon includes its smallest bounding box. This speeds up certain search operations, although computing the bounding box adds overhead while constructing new polygons. /// Values of type polygon are specified using any of the following syntaxes: /// -/// ``` +/// ```text /// ( ( x1 , y1 ) , ... , ( xn , yn ) ) /// ( x1 , y1 ) , ... , ( xn , yn ) /// ( x1 , y1 , ... , xn , yn ) @@ -76,7 +75,7 @@ impl FromStr for PgPolygon { type Err = Error; fn from_str(s: &str) -> Result { - let sanitised = s.replace(&['(', ')', '[', ']', ' '][..], ""); + let sanitised = s.replace(['(', ')', '[', ']', ' '], ""); let mut parts = sanitised.split(","); let mut points = vec![]; From c538aa4a1b6d1270994c94f1166187ee601cdbe7 Mon Sep 17 00:00:00 2001 From: James Holman Date: Mon, 2 Sep 2024 19:43:00 +1000 Subject: [PATCH 32/64] test: fix type tests for polygon --- 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 58b56a5cc7..3de0a8691c 100644 --- a/tests/postgres/types.rs +++ b/tests/postgres/types.rs @@ -546,7 +546,7 @@ test_type!(_path>(Postgres, )); #[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] -test_type!(polygon(Postgres, +test_type!(polygon(Postgres, "polygon('((-2,-3),(-1,-3),(-1,-1),(1,1),(1,3),(2,3),(2,-3),(1,-3),(1,0),(-1,0),(-1,-2),(-2,-2))')" @= sqlx::postgres::types::PgPolygon { points: vec![ PgPoint { x: -2., y: -3. }, PgPoint { x: -1., y: -3. }, PgPoint { x: -1., y: -1. }, PgPoint { x: 1., y: 1. }, PgPoint { x: 1., y: 3. }, PgPoint { x: 2., y: 3. }, PgPoint { x: 2., y: -3. }, PgPoint { x: 1., y: -3. }, From 92a4bdba3e403d88c658db107b5b6a4b07d067e5 Mon Sep 17 00:00:00 2001 From: James Holman Date: Mon, 2 Sep 2024 19:47:14 +1000 Subject: [PATCH 33/64] fix: circle from bytes --- sqlx-postgres/src/types/geometry/circle.rs | 5 --- sqlx-postgres/src/types/geometry/path.rs | 36 +++++++++------------- 2 files changed, 15 insertions(+), 26 deletions(-) diff --git a/sqlx-postgres/src/types/geometry/circle.rs b/sqlx-postgres/src/types/geometry/circle.rs index 815b874a64..7ac655da2c 100644 --- a/sqlx-postgres/src/types/geometry/circle.rs +++ b/sqlx-postgres/src/types/geometry/circle.rs @@ -99,13 +99,8 @@ impl FromStr for PgCircle { impl PgCircle { fn from_bytes(mut bytes: &[u8]) -> Result { let x = bytes.get_f64(); -<<<<<<< HEAD - let b = bytes.get_f64(); - let y = bytes.get_f64(); -======= let y = bytes.get_f64(); let r = bytes.get_f64(); ->>>>>>> 0c1caacd (fix: unit tests) Ok(PgCircle { x, y, r }) } diff --git a/sqlx-postgres/src/types/geometry/path.rs b/sqlx-postgres/src/types/geometry/path.rs index a07766d9fa..ccb2d9a0d7 100644 --- a/sqlx-postgres/src/types/geometry/path.rs +++ b/sqlx-postgres/src/types/geometry/path.rs @@ -109,28 +109,24 @@ impl PgPath { } } - fn from_bytes(mut bytes: &[u8]) -> Result { - let header = Header::try_read(&mut bytes).map_err(|s| Error::Decode(s.into()))?; + fn from_bytes(mut bytes: &[u8]) -> Result { + let header = Header::try_read(&mut bytes)?; if bytes.len() != header.data_size() { - return Err(Error::Decode( - format!( - "expected {} bytes after header, got {}", - header.data_size(), - bytes.len() - ) - .into(), - )); + return Err(format!( + "expected {} bytes after header, got {}", + header.data_size(), + bytes.len() + ) + .into()); } if bytes.len() % BYTE_WIDTH * 2 != 0 { - return Err(Error::Decode( - format!( - "data length not divisible by pairs of {BYTE_WIDTH}: {}", - bytes.len() - ) - .into(), - )); + return Err(format!( + "data length not divisible by pairs of {BYTE_WIDTH}: {}", + bytes.len() + ) + .into()); } let mut out_points = Vec::with_capacity(bytes.len() / BYTE_WIDTH * 2); @@ -147,12 +143,10 @@ impl PgPath { }) } - fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), Error> { + fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), BoxDynError> { let header = self.header(); buff.reserve(header.data_size()); - header - .try_write(buff) - .map_err(|s| Error::Encode(s.into()))?; + header.try_write(buff)?; for point in &self.points { buff.extend_from_slice(&point.x.to_be_bytes()); From 305e95cc9c961ce5393fb4654ef1affedb275fdc Mon Sep 17 00:00:00 2001 From: James Holman Date: Mon, 2 Sep 2024 19:49:15 +1000 Subject: [PATCH 34/64] fix: remove double decodes --- sqlx-postgres/src/types/geometry/line.rs | 4 +-- .../src/types/geometry/line_segment.rs | 4 +-- sqlx-postgres/src/types/geometry/point.rs | 2 +- sqlx-postgres/src/types/geometry/polygon.rs | 36 ++++++++----------- 4 files changed, 20 insertions(+), 26 deletions(-) diff --git a/sqlx-postgres/src/types/geometry/line.rs b/sqlx-postgres/src/types/geometry/line.rs index a883c570e0..67391f5f70 100644 --- a/sqlx-postgres/src/types/geometry/line.rs +++ b/sqlx-postgres/src/types/geometry/line.rs @@ -90,14 +90,14 @@ impl FromStr for PgLine { } impl PgLine { - fn from_bytes(mut bytes: &[u8]) -> Result { + 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<(), Error> { + 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()); diff --git a/sqlx-postgres/src/types/geometry/line_segment.rs b/sqlx-postgres/src/types/geometry/line_segment.rs index 03e04b1ae9..89eef5a09a 100644 --- a/sqlx-postgres/src/types/geometry/line_segment.rs +++ b/sqlx-postgres/src/types/geometry/line_segment.rs @@ -108,7 +108,7 @@ impl FromStr for PgLSeg { } impl PgLSeg { - fn from_bytes(mut bytes: &[u8]) -> Result { + fn from_bytes(mut bytes: &[u8]) -> Result { let x1 = bytes.get_f64(); let y1 = bytes.get_f64(); let x2 = bytes.get_f64(); @@ -117,7 +117,7 @@ impl PgLSeg { Ok(PgLSeg { x1, y1, x2, y2 }) } - fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), Error> { + fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), BoxDynError> { buff.extend_from_slice(&self.x1.to_be_bytes()); buff.extend_from_slice(&self.y1.to_be_bytes()); buff.extend_from_slice(&self.x2.to_be_bytes()); diff --git a/sqlx-postgres/src/types/geometry/point.rs b/sqlx-postgres/src/types/geometry/point.rs index f4ccedcbf7..60af50a05b 100644 --- a/sqlx-postgres/src/types/geometry/point.rs +++ b/sqlx-postgres/src/types/geometry/point.rs @@ -89,7 +89,7 @@ impl PgPoint { Ok(PgPoint { x, y }) } - fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), String> { + 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(()) diff --git a/sqlx-postgres/src/types/geometry/polygon.rs b/sqlx-postgres/src/types/geometry/polygon.rs index fa12bdfcab..11bce2776d 100644 --- a/sqlx-postgres/src/types/geometry/polygon.rs +++ b/sqlx-postgres/src/types/geometry/polygon.rs @@ -105,28 +105,24 @@ impl PgPolygon { } } - fn from_bytes(mut bytes: &[u8]) -> Result { - let header = Header::try_read(&mut bytes).map_err(|s| Error::Decode(s.into()))?; + fn from_bytes(mut bytes: &[u8]) -> Result { + let header = Header::try_read(&mut bytes)?; if bytes.len() != header.data_size() { - return Err(Error::Decode( - format!( - "expected {} bytes after header, got {}", - header.data_size(), - bytes.len() - ) - .into(), - )); + return Err(format!( + "expected {} bytes after header, got {}", + header.data_size(), + bytes.len() + ) + .into()); } if bytes.len() % BYTE_WIDTH * 2 != 0 { - return Err(Error::Decode( - format!( - "data length not divisible by pairs of {BYTE_WIDTH}: {}", - bytes.len() - ) - .into(), - )); + return Err(format!( + "data length not divisible by pairs of {BYTE_WIDTH}: {}", + bytes.len() + ) + .into()); } let mut out_points = Vec::with_capacity(bytes.len() / BYTE_WIDTH * 2); @@ -140,12 +136,10 @@ impl PgPolygon { Ok(PgPolygon { points: out_points }) } - fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), Error> { + fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), BoxDynError> { let header = self.header(); buff.reserve(header.data_size()); - header - .try_write(buff) - .map_err(|s| Error::Encode(s.into()))?; + header.try_write(buff)?; for point in &self.points { buff.extend_from_slice(&point.x.to_be_bytes()); From 2d285142d0046b5874dcfcb801912862cff3b60a Mon Sep 17 00:00:00 2001 From: James Holman Date: Tue, 3 Sep 2024 18:05:05 +1000 Subject: [PATCH 35/64] fix: circle test --- 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 3de0a8691c..89e72dca04 100644 --- a/tests/postgres/types.rs +++ b/tests/postgres/types.rs @@ -564,7 +564,7 @@ test_type!(circle(Postgres, #[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] test_type!(_circle>(Postgres, - "array[circle('<(1,2,3}'),circle('{1.1, 2.2, 3.3}')]" @= vec![sqlx::postgres::types::PgCircle { x: 1., y: 2., r: 3. }, sqlx::postgres::types::PgCircle { x: 1.1, y: 2.2, r: 3.3 }], + "array[circle('<(1,2,3)>'),circle('(1.1, 2.2, 3.3)')]" @= vec![sqlx::postgres::types::PgCircle { x: 1., y: 2., r: 3. }, sqlx::postgres::types::PgCircle { x: 1.1, y: 2.2, r: 3.3 }], )); #[cfg(feature = "rust_decimal")] From fc6f3f32c14cf2c02956761133716246e0ec4b3e Mon Sep 17 00:00:00 2001 From: James Holman Date: Tue, 3 Sep 2024 18:06:04 +1000 Subject: [PATCH 36/64] test: boxes sort --- 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 89e72dca04..2473aea548 100644 --- a/tests/postgres/types.rs +++ b/tests/postgres/types.rs @@ -526,7 +526,7 @@ test_type!(_lseg>(Postgres, #[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] test_type!(box(Postgres, - "box('((1.0, 2.0), (3.0,4.0))')" @= sqlx::postgres::types::PgBox { x1: 1., y1: 2., x2: 3. , y2: 4.}, + "box('((1.0, 2.0), (3.0,4.0))')" @= sqlx::postgres::types::PgBox { x1: 3., y1: 4., x2: 1. , y2: 2.}, )); #[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] From 066262810654b8da19622eab236efe7b1de1c8e4 Mon Sep 17 00:00:00 2001 From: James Holman Date: Tue, 3 Sep 2024 18:14:39 +1000 Subject: [PATCH 37/64] docs: update doc headerws --- sqlx-postgres/src/types/geometry/box.rs | 2 +- sqlx-postgres/src/types/geometry/line_segment.rs | 2 +- sqlx-postgres/src/types/geometry/path.rs | 2 +- sqlx-postgres/src/types/geometry/point.rs | 2 +- sqlx-postgres/src/types/geometry/polygon.rs | 11 ++++++----- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/sqlx-postgres/src/types/geometry/box.rs b/sqlx-postgres/src/types/geometry/box.rs index 0d9bf768e6..42c877adee 100644 --- a/sqlx-postgres/src/types/geometry/box.rs +++ b/sqlx-postgres/src/types/geometry/box.rs @@ -9,7 +9,7 @@ use std::str::FromStr; const ERROR: &str = "error decoding BOX"; -/// Postgres Geometric Box type +/// ## Postgres Geometric Box type /// /// Description: Rectangular box /// Representation: `((x1,y1),(x2,y2))` diff --git a/sqlx-postgres/src/types/geometry/line_segment.rs b/sqlx-postgres/src/types/geometry/line_segment.rs index 89eef5a09a..da2d405d60 100644 --- a/sqlx-postgres/src/types/geometry/line_segment.rs +++ b/sqlx-postgres/src/types/geometry/line_segment.rs @@ -9,7 +9,7 @@ use std::str::FromStr; const ERROR: &str = "error decoding LSEG"; -/// Postgres Geometric Line Segment type +/// ## Postgres Geometric Line Segment type /// /// Description: Finite line segment /// Representation: `((x1,y1),(x2,y2))` diff --git a/sqlx-postgres/src/types/geometry/path.rs b/sqlx-postgres/src/types/geometry/path.rs index ccb2d9a0d7..4f8a4d4c0a 100644 --- a/sqlx-postgres/src/types/geometry/path.rs +++ b/sqlx-postgres/src/types/geometry/path.rs @@ -9,7 +9,7 @@ use std::str::FromStr; const BYTE_WIDTH: usize = 8; -/// Postgres Geometric Path type +/// ## Postgres Geometric Path type /// /// Description: Open path or Closed path (similar to polygon) /// Representation: Open `[(x1,y1),...]`, Closed `((x1,y1),...)` diff --git a/sqlx-postgres/src/types/geometry/point.rs b/sqlx-postgres/src/types/geometry/point.rs index 60af50a05b..028a97b6f6 100644 --- a/sqlx-postgres/src/types/geometry/point.rs +++ b/sqlx-postgres/src/types/geometry/point.rs @@ -7,7 +7,7 @@ use sqlx_core::bytes::Buf; use sqlx_core::Error; use std::str::FromStr; -/// Postgres Geometric Point type +/// ## Postgres Geometric Point type /// /// Description: Point on a plane /// Representation: `(x, y)` diff --git a/sqlx-postgres/src/types/geometry/polygon.rs b/sqlx-postgres/src/types/geometry/polygon.rs index 11bce2776d..d4064da7c6 100644 --- a/sqlx-postgres/src/types/geometry/polygon.rs +++ b/sqlx-postgres/src/types/geometry/polygon.rs @@ -5,11 +5,12 @@ use crate::types::{PgPoint, Type}; use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, Postgres}; use sqlx_core::bytes::Buf; use sqlx_core::Error; +use std::mem; use std::str::FromStr; -const BYTE_WIDTH: usize = 8; +const BYTE_WIDTH: usize = mem::size_of::(); -/// Postgres Geometric Polygon type +/// ## Postgres Geometric Polygon type /// /// Description: Polygon (similar to closed polygon) /// Representation: `((x1,y1),...)` @@ -157,17 +158,17 @@ impl PgPolygon { } impl Header { - const PACKED_WIDTH: usize = size_of::() + size_of::(); + const HEADER_WIDTH: usize = size_of::() + size_of::(); fn data_size(&self) -> usize { self.length * BYTE_WIDTH * 2 } fn try_read(buf: &mut &[u8]) -> Result { - if buf.len() < Self::PACKED_WIDTH { + if buf.len() < Self::HEADER_WIDTH { return Err(format!( "expected polygon data to contain at least {} bytes, got {}", - Self::PACKED_WIDTH, + Self::HEADER_WIDTH, buf.len() )); } From 65184f71ba54dea38e3c5c5a67e4d2aca7c4a63b Mon Sep 17 00:00:00 2001 From: James Holman Date: Tue, 3 Sep 2024 18:17:27 +1000 Subject: [PATCH 38/64] test: invalid box syntax --- 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 2473aea548..f07a6d02d6 100644 --- a/tests/postgres/types.rs +++ b/tests/postgres/types.rs @@ -531,7 +531,7 @@ test_type!(box(Postgres, #[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] test_type!(_box>(Postgres, - "array[box('1,2,3,4'),box('((1.1, 2.2), (3.3, 4.4)]')]" @= vec![sqlx::postgres::types::PgBox { x1: 1., y1: 2., x2: 3., y2: 4. }, sqlx::postgres::types::PgBox { x1: 1.1, y1: 2.2, x2: 3.3, y2: 4.4 }], + "array[box('1,2,3,4'),box('((1.1, 2.2), (3.3, 4.4))')]" @= vec![sqlx::postgres::types::PgBox { x1: 1., y1: 2., x2: 3., y2: 4. }, sqlx::postgres::types::PgBox { x1: 1.1, y1: 2.2, x2: 3.3, y2: 4.4 }], )); #[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] @@ -564,7 +564,7 @@ test_type!(circle(Postgres, #[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] test_type!(_circle>(Postgres, - "array[circle('<(1,2,3)>'),circle('(1.1, 2.2, 3.3)')]" @= vec![sqlx::postgres::types::PgCircle { x: 1., y: 2., r: 3. }, sqlx::postgres::types::PgCircle { x: 1.1, y: 2.2, r: 3.3 }], + "array[circle('<(1,2),3>'),circle('(1.1, 2.2, 3.3)')]" @= vec![sqlx::postgres::types::PgCircle { x: 1., y: 2., r: 3. }, sqlx::postgres::types::PgCircle { x: 1.1, y: 2.2, r: 3.3 }], )); #[cfg(feature = "rust_decimal")] From 5a165dfa0faafdf3140bda78d9f9467eac1c48ca Mon Sep 17 00:00:00 2001 From: James Holman Date: Tue, 3 Sep 2024 19:00:07 +1000 Subject: [PATCH 39/64] fix: circle syntax --- 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 f07a6d02d6..0e4a5bf6b5 100644 --- a/tests/postgres/types.rs +++ b/tests/postgres/types.rs @@ -564,7 +564,7 @@ test_type!(circle(Postgres, #[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] test_type!(_circle>(Postgres, - "array[circle('<(1,2),3>'),circle('(1.1, 2.2, 3.3)')]" @= vec![sqlx::postgres::types::PgCircle { x: 1., y: 2., r: 3. }, sqlx::postgres::types::PgCircle { x: 1.1, y: 2.2, r: 3.3 }], + "array[circle('<(1,2),3>'),circle('(1.1, 2.2), 3.3')]" @= vec![sqlx::postgres::types::PgCircle { x: 1., y: 2., r: 3. }, sqlx::postgres::types::PgCircle { x: 1.1, y: 2.2, r: 3.3 }], )); #[cfg(feature = "rust_decimal")] From 95ec8094e4f27628cf9e43183c2c37778ede7853 Mon Sep 17 00:00:00 2001 From: James Holman Date: Tue, 3 Sep 2024 19:54:02 +1000 Subject: [PATCH 40/64] test: box ordering in array test --- 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 0e4a5bf6b5..2aa27bea9d 100644 --- a/tests/postgres/types.rs +++ b/tests/postgres/types.rs @@ -531,7 +531,7 @@ test_type!(box(Postgres, #[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] test_type!(_box>(Postgres, - "array[box('1,2,3,4'),box('((1.1, 2.2), (3.3, 4.4))')]" @= vec![sqlx::postgres::types::PgBox { x1: 1., y1: 2., x2: 3., y2: 4. }, sqlx::postgres::types::PgBox { x1: 1.1, y1: 2.2, x2: 3.3, y2: 4.4 }], + "array[box('1,2,3,4'),box('((1.1, 2.2), (3.3, 4.4))')]" @= vec![sqlx::postgres::types::PgBox { x1: 3., y1: 4., x2: 1., y2: 2. }, sqlx::postgres::types::PgBox { x1: 3.3, y1: 4.4, x2: 1.1, y2: 2.2 }], )); #[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] From 24486e263a8a45727e4f90934ec70207b1b26148 Mon Sep 17 00:00:00 2001 From: James Holman Date: Tue, 3 Sep 2024 20:39:03 +1000 Subject: [PATCH 41/64] docs: remove output comments --- sqlx-postgres/src/types/geometry/box.rs | 1 - sqlx-postgres/src/types/geometry/circle.rs | 2 -- sqlx-postgres/src/types/geometry/line.rs | 2 +- sqlx-postgres/src/types/geometry/line_segment.rs | 3 --- sqlx-postgres/src/types/geometry/path.rs | 2 -- sqlx-postgres/src/types/geometry/polygon.rs | 1 - 6 files changed, 1 insertion(+), 10 deletions(-) diff --git a/sqlx-postgres/src/types/geometry/box.rs b/sqlx-postgres/src/types/geometry/box.rs index 42c877adee..503c9fd99e 100644 --- a/sqlx-postgres/src/types/geometry/box.rs +++ b/sqlx-postgres/src/types/geometry/box.rs @@ -22,7 +22,6 @@ const ERROR: &str = "error decoding BOX"; /// x1 , y1 , x2 , y2 /// ``` /// where `(x1,y1) and (x2,y2)` are any two opposite corners of the box. -/// Boxes are output using the second syntax. /// Any two opposite corners can be supplied on input, but the values will be reordered as needed to store the upper right and lower left corners, in that order. /// /// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-GEOMETRIC-BOXES diff --git a/sqlx-postgres/src/types/geometry/circle.rs b/sqlx-postgres/src/types/geometry/circle.rs index 7ac655da2c..12c4e56989 100644 --- a/sqlx-postgres/src/types/geometry/circle.rs +++ b/sqlx-postgres/src/types/geometry/circle.rs @@ -22,8 +22,6 @@ const ERROR: &str = "error decoding CIRCLE"; /// ``` /// where `(x,y)` is the center point and r is the radius of the circle. /// -/// Circles are output using the first syntax. -/// /// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-CIRCLE #[derive(Debug, Clone, PartialEq)] pub struct PgCircle { diff --git a/sqlx-postgres/src/types/geometry/line.rs b/sqlx-postgres/src/types/geometry/line.rs index 67391f5f70..daa487bf45 100644 --- a/sqlx-postgres/src/types/geometry/line.rs +++ b/sqlx-postgres/src/types/geometry/line.rs @@ -14,7 +14,7 @@ const ERROR: &str = "error decoding LINE"; /// 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. Values of type line are input and output in the following form: +/// 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)] diff --git a/sqlx-postgres/src/types/geometry/line_segment.rs b/sqlx-postgres/src/types/geometry/line_segment.rs index da2d405d60..b4ab0d30ac 100644 --- a/sqlx-postgres/src/types/geometry/line_segment.rs +++ b/sqlx-postgres/src/types/geometry/line_segment.rs @@ -24,9 +24,6 @@ const ERROR: &str = "error decoding LSEG"; /// ``` /// where `(x1,y1) and (x2,y2)` are the end points of the line segment. /// -/// Line segments are output using the first syntax. -/// -/// /// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-LSEG #[derive(Debug, Clone, PartialEq)] pub struct PgLSeg { diff --git a/sqlx-postgres/src/types/geometry/path.rs b/sqlx-postgres/src/types/geometry/path.rs index 4f8a4d4c0a..93bcfa70b4 100644 --- a/sqlx-postgres/src/types/geometry/path.rs +++ b/sqlx-postgres/src/types/geometry/path.rs @@ -26,8 +26,6 @@ const BYTE_WIDTH: usize = 8; /// where the points are the end points of the line segments comprising the path. Square brackets `([])` indicate an open path, while parentheses `(())` indicate a closed path. /// When the outermost parentheses are omitted, as in the third through fifth syntaxes, a closed path is assumed. /// -/// Paths are output using the first or second syntax, as appropriate. -/// /// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-GEOMETRIC-PATHS #[derive(Debug, Clone, PartialEq)] pub struct PgPath { diff --git a/sqlx-postgres/src/types/geometry/polygon.rs b/sqlx-postgres/src/types/geometry/polygon.rs index d4064da7c6..bb51391a2d 100644 --- a/sqlx-postgres/src/types/geometry/polygon.rs +++ b/sqlx-postgres/src/types/geometry/polygon.rs @@ -27,7 +27,6 @@ const BYTE_WIDTH: usize = mem::size_of::(); /// ``` /// /// where the points are the end points of the line segments comprising the boundary of the polygon. -/// Polygons are output using the first syntax. /// /// Seeh ttps://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-POLYGON #[derive(Debug, Clone, PartialEq)] From d3082c8c6c971c267f8297a209442d686fc8c33d Mon Sep 17 00:00:00 2001 From: James Holman Date: Tue, 3 Sep 2024 20:49:56 +1000 Subject: [PATCH 42/64] fix: usize max in max lengths --- sqlx-postgres/src/types/geometry/path.rs | 6 +++++- sqlx-postgres/src/types/geometry/polygon.rs | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/sqlx-postgres/src/types/geometry/path.rs b/sqlx-postgres/src/types/geometry/path.rs index 93bcfa70b4..85188d0710 100644 --- a/sqlx-postgres/src/types/geometry/path.rs +++ b/sqlx-postgres/src/types/geometry/path.rs @@ -5,6 +5,7 @@ use crate::types::{PgPoint, Type}; use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, Postgres}; use sqlx_core::bytes::Buf; use sqlx_core::Error; +use std::i32; use std::str::FromStr; const BYTE_WIDTH: usize = 8; @@ -181,7 +182,10 @@ impl Header { let length = buf.get_i32(); let length = usize::try_from(length).ok().ok_or_else(|| { - format!("received PATH data with greater than expected length: {length}") + format!( + "received PATH data length: {length}. Expected length between 0 and {}", + usize::MAX + ) })?; Ok(Self { diff --git a/sqlx-postgres/src/types/geometry/polygon.rs b/sqlx-postgres/src/types/geometry/polygon.rs index bb51391a2d..f7ba4f8f05 100644 --- a/sqlx-postgres/src/types/geometry/polygon.rs +++ b/sqlx-postgres/src/types/geometry/polygon.rs @@ -175,7 +175,10 @@ impl Header { let length = buf.get_i32(); let length = usize::try_from(length).ok().ok_or_else(|| { - format!("received polygon data with greater than expected length: {length}") + format!( + "received polygon with length: {length}. Expected length between 0 and {}", + usize::MAX + ) })?; Ok(Self { length }) From 8207d8912489657438ecec63c745c97b159ba886 Mon Sep 17 00:00:00 2001 From: James Holman Date: Tue, 3 Sep 2024 20:54:22 +1000 Subject: [PATCH 43/64] fix: handle uneven number of points --- sqlx-postgres/src/types/geometry/path.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/sqlx-postgres/src/types/geometry/path.rs b/sqlx-postgres/src/types/geometry/path.rs index 85188d0710..0cda294d71 100644 --- a/sqlx-postgres/src/types/geometry/path.rs +++ b/sqlx-postgres/src/types/geometry/path.rs @@ -5,10 +5,10 @@ use crate::types::{PgPoint, Type}; use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, Postgres}; use sqlx_core::bytes::Buf; use sqlx_core::Error; -use std::i32; +use std::mem; use std::str::FromStr; -const BYTE_WIDTH: usize = 8; +const BYTE_WIDTH: usize = mem::size_of::(); /// ## Postgres Geometric Path type /// @@ -90,6 +90,12 @@ impl FromStr for PgPath { points.push(point); } + if parts.next().is_some() { + return Err(Error::Decode( + format!("Unmatched pair in path: {}", s).into(), + )); + } + if !points.is_empty() { return Ok(PgPath { points, closed }); } @@ -163,17 +169,17 @@ impl PgPath { } impl Header { - const PACKED_WIDTH: usize = size_of::() + size_of::(); + const HEADER_WIDTH: usize = size_of::() + size_of::(); fn data_size(&self) -> usize { self.length * BYTE_WIDTH * 2 } fn try_read(buf: &mut &[u8]) -> Result { - if buf.len() < Self::PACKED_WIDTH { + if buf.len() < Self::HEADER_WIDTH { return Err(format!( "expected PATH data to contain at least {} bytes, got {}", - Self::PACKED_WIDTH, + Self::HEADER_WIDTH, buf.len() )); } From 4dd55c3c35ebb6cc5cb7cb51669a83115160d288 Mon Sep 17 00:00:00 2001 From: James Holman Date: Tue, 3 Sep 2024 20:59:28 +1000 Subject: [PATCH 44/64] fix: capacity --- sqlx-postgres/src/types/geometry/path.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sqlx-postgres/src/types/geometry/path.rs b/sqlx-postgres/src/types/geometry/path.rs index 0cda294d71..351a4cf417 100644 --- a/sqlx-postgres/src/types/geometry/path.rs +++ b/sqlx-postgres/src/types/geometry/path.rs @@ -134,7 +134,8 @@ impl PgPath { .into()); } - let mut out_points = Vec::with_capacity(bytes.len() / BYTE_WIDTH * 2); + let mut out_points = Vec::with_capacity(bytes.len() / (BYTE_WIDTH * 2)); + while bytes.has_remaining() { let point = PgPoint { x: bytes.get_f64(), From 8ec8142629287313647a926a99736b3efecda191 Mon Sep 17 00:00:00 2001 From: James Holman Date: Tue, 3 Sep 2024 21:00:31 +1000 Subject: [PATCH 45/64] fix: order of operations --- sqlx-postgres/src/types/geometry/polygon.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlx-postgres/src/types/geometry/polygon.rs b/sqlx-postgres/src/types/geometry/polygon.rs index f7ba4f8f05..9007bf2124 100644 --- a/sqlx-postgres/src/types/geometry/polygon.rs +++ b/sqlx-postgres/src/types/geometry/polygon.rs @@ -125,7 +125,7 @@ impl PgPolygon { .into()); } - let mut out_points = Vec::with_capacity(bytes.len() / BYTE_WIDTH * 2); + let mut out_points = Vec::with_capacity(bytes.len() / (BYTE_WIDTH * 2)); while bytes.has_remaining() { let point = PgPoint { x: bytes.get_f64(), From 99d83687de2cc6772dd966820605692b2b76bc60 Mon Sep 17 00:00:00 2001 From: James Holman Date: Tue, 3 Sep 2024 21:01:39 +1000 Subject: [PATCH 46/64] fix: uneven pairs in polygons --- sqlx-postgres/src/types/geometry/polygon.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sqlx-postgres/src/types/geometry/polygon.rs b/sqlx-postgres/src/types/geometry/polygon.rs index 9007bf2124..7815273661 100644 --- a/sqlx-postgres/src/types/geometry/polygon.rs +++ b/sqlx-postgres/src/types/geometry/polygon.rs @@ -88,6 +88,12 @@ impl FromStr for PgPolygon { points.push(point); } + if parts.next().is_some() { + return Err(Error::Decode( + format!("Unmatched pair in path: {}", s).into(), + )); + } + if !points.is_empty() { return Ok(PgPolygon { points }); } From 2ebb0fbcf656cee04545dbc1d76fdb6b0bd20228 Mon Sep 17 00:00:00 2001 From: James Holman Date: Tue, 3 Sep 2024 21:22:39 +1000 Subject: [PATCH 47/64] fix: throw on uneven points --- sqlx-postgres/src/types/geometry/path.rs | 56 +++++++++++++++++---- sqlx-postgres/src/types/geometry/polygon.rs | 22 ++++---- 2 files changed, 58 insertions(+), 20 deletions(-) diff --git a/sqlx-postgres/src/types/geometry/path.rs b/sqlx-postgres/src/types/geometry/path.rs index 351a4cf417..8037f5cb48 100644 --- a/sqlx-postgres/src/types/geometry/path.rs +++ b/sqlx-postgres/src/types/geometry/path.rs @@ -78,24 +78,26 @@ impl FromStr for PgPath { fn from_str(s: &str) -> Result { let closed = !s.contains("["); let sanitised = s.replace(['(', ')', '[', ']', ' '], ""); - let mut parts = sanitised.split(","); + let parts = sanitised.split(",").collect::>(); let mut points = vec![]; - while let (Some(x_str), Some(y_str)) = (parts.next(), parts.next()) { - let x = parse_float_from_str(x_str, "could not get x")?; - let y = parse_float_from_str(y_str, "could not get y")?; - - let point = PgPoint { x, y }; - points.push(point); - } - - if parts.next().is_some() { + if parts.len() % 2 != 0 { return Err(Error::Decode( format!("Unmatched pair in path: {}", s).into(), )); } + for chunk in parts.chunks_exact(2) { + if let [x_str, y_str] = chunk { + let x = parse_float_from_str(x_str, "could not get x")?; + let y = parse_float_from_str(y_str, "could not get y")?; + + let point = PgPoint { x, y }; + points.push(point); + } + } + if !points.is_empty() { return Ok(PgPath { points, closed }); } @@ -242,6 +244,11 @@ mod path_tests { 64, 16, 0, 0, 0, 0, 0, 0, ]; + const PATH_UNEVEN_POINTS: &[u8] = &[ + 0, 0, 0, 0, 2, 63, 240, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 64, 8, 0, 0, 0, 0, 0, 0, + 64, 16, 0, 0, + ]; + #[test] fn can_deserialise_path_type_bytes_closed() { let path = PgPath::from_bytes(PATH_CLOSED_BYTES).unwrap(); @@ -254,6 +261,19 @@ mod path_tests { ) } + #[test] + fn cannot_deserialise_path_type_uneven_point_bytes() { + let path = PgPath::from_bytes(PATH_UNEVEN_POINTS); + assert!(path.is_err()); + + if let Err(err) = path { + assert_eq!( + err.to_string(), + format!("expected 32 bytes after header, got 28") + ) + } + } + #[test] fn can_deserialise_path_type_bytes_open() { let path = PgPath::from_bytes(PATH_OPEN_BYTES).unwrap(); @@ -277,6 +297,22 @@ mod path_tests { } ); } + + #[test] + fn cannot_deserialise_path_type_str_uneven_points_first_syntax() { + let input_str = "[( 1, 2), (3)]"; + let path = PgPath::from_str(input_str); + + assert!(path.is_err()); + + if let Err(err) = path { + assert_eq!( + err.to_string(), + format!("error occurred while decoding: Unmatched pair in path: {input_str}") + ) + } + } + #[test] fn can_deserialise_path_type_str_second_syntax() { let path = PgPath::from_str("(( 1, 2), (3, 4 ))").unwrap(); diff --git a/sqlx-postgres/src/types/geometry/polygon.rs b/sqlx-postgres/src/types/geometry/polygon.rs index 7815273661..9b1cfae63b 100644 --- a/sqlx-postgres/src/types/geometry/polygon.rs +++ b/sqlx-postgres/src/types/geometry/polygon.rs @@ -76,24 +76,26 @@ impl FromStr for PgPolygon { fn from_str(s: &str) -> Result { let sanitised = s.replace(['(', ')', '[', ']', ' '], ""); - let mut parts = sanitised.split(","); + let parts = sanitised.split(",").collect::>(); let mut points = vec![]; - while let (Some(x_str), Some(y_str)) = (parts.next(), parts.next()) { - let x = parse_float_from_str(x_str, "could not get x")?; - let y = parse_float_from_str(y_str, "could not get y")?; - - let point = PgPoint { x, y }; - points.push(point); - } - - if parts.next().is_some() { + if parts.len() % 2 != 0 { return Err(Error::Decode( format!("Unmatched pair in path: {}", s).into(), )); } + for chunk in parts.chunks_exact(2) { + if let [x_str, y_str] = chunk { + let x = parse_float_from_str(x_str, "could not get x")?; + let y = parse_float_from_str(y_str, "could not get y")?; + + let point = PgPoint { x, y }; + points.push(point); + } + } + if !points.is_empty() { return Ok(PgPolygon { points }); } From cffcec9174d7e3985460474bf3e9827bad1fa127 Mon Sep 17 00:00:00 2001 From: James Holman Date: Tue, 3 Sep 2024 21:23:25 +1000 Subject: [PATCH 48/64] fix: unmatched pair errors --- sqlx-postgres/src/types/geometry/path.rs | 2 +- sqlx-postgres/src/types/geometry/polygon.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sqlx-postgres/src/types/geometry/path.rs b/sqlx-postgres/src/types/geometry/path.rs index 8037f5cb48..a8709d576c 100644 --- a/sqlx-postgres/src/types/geometry/path.rs +++ b/sqlx-postgres/src/types/geometry/path.rs @@ -84,7 +84,7 @@ impl FromStr for PgPath { if parts.len() % 2 != 0 { return Err(Error::Decode( - format!("Unmatched pair in path: {}", s).into(), + format!("Unmatched pair in PATH: {}", s).into(), )); } diff --git a/sqlx-postgres/src/types/geometry/polygon.rs b/sqlx-postgres/src/types/geometry/polygon.rs index 9b1cfae63b..fecc0f4f17 100644 --- a/sqlx-postgres/src/types/geometry/polygon.rs +++ b/sqlx-postgres/src/types/geometry/polygon.rs @@ -82,7 +82,7 @@ impl FromStr for PgPolygon { if parts.len() % 2 != 0 { return Err(Error::Decode( - format!("Unmatched pair in path: {}", s).into(), + format!("Unmatched pair in POLYGON: {}", s).into(), )); } From 341650c1d7640b7087837bd328a63ef855ec0687 Mon Sep 17 00:00:00 2001 From: James Holman Date: Wed, 4 Sep 2024 08:20:20 +1000 Subject: [PATCH 49/64] fix: box dyn errors for from strings --- sqlx-postgres/src/types/geometry/box.rs | 18 +++++------------- sqlx-postgres/src/types/geometry/circle.rs | 14 ++++---------- sqlx-postgres/src/types/geometry/line.rs | 14 ++++---------- .../src/types/geometry/line_segment.rs | 18 +++++------------- sqlx-postgres/src/types/geometry/path.rs | 2 +- sqlx-postgres/src/types/geometry/point.rs | 6 ++---- 6 files changed, 21 insertions(+), 51 deletions(-) diff --git a/sqlx-postgres/src/types/geometry/box.rs b/sqlx-postgres/src/types/geometry/box.rs index 503c9fd99e..32316bb7e4 100644 --- a/sqlx-postgres/src/types/geometry/box.rs +++ b/sqlx-postgres/src/types/geometry/box.rs @@ -66,7 +66,7 @@ impl<'q> Encode<'q, Postgres> for PgBox { } impl FromStr for PgBox { - type Err = Error; + type Err = BoxDynError; fn from_str(s: &str) -> Result { let sanitised = s.replace(['(', ')', '[', ']', ' '], ""); @@ -75,30 +75,22 @@ impl FromStr for PgBox { let x1 = parts .next() .and_then(|s| s.parse::().ok()) - .ok_or(Error::Decode( - format!("{}: could not get x1 from {}", ERROR, s).into(), - ))?; + .ok_or_else(|| format!("{}: could not get x1 from {}", ERROR, s))?; let y1 = parts .next() .and_then(|s| s.parse::().ok()) - .ok_or(Error::Decode( - format!("{}: could not get y1 from {}", ERROR, s).into(), - ))?; + .ok_or_else(|| format!("{}: could not get y1 from {}", ERROR, s))?; let x2 = parts .next() .and_then(|s| s.parse::().ok()) - .ok_or(Error::Decode( - format!("{}: could not get x2 from {}", ERROR, s).into(), - ))?; + .ok_or_else(|| format!("{}: could not get x2 from {}", ERROR, s))?; let y2 = parts .next() .and_then(|s| s.parse::().ok()) - .ok_or(Error::Decode( - format!("{}: could not get y2 from {}", ERROR, s).into(), - ))?; + .ok_or_else(|| format!("{}: could not get y2 from {}", ERROR, s))?; Ok(PgBox { x1, y1, x2, y2 }) } diff --git a/sqlx-postgres/src/types/geometry/circle.rs b/sqlx-postgres/src/types/geometry/circle.rs index 12c4e56989..446505d8ab 100644 --- a/sqlx-postgres/src/types/geometry/circle.rs +++ b/sqlx-postgres/src/types/geometry/circle.rs @@ -63,7 +63,7 @@ impl<'q> Encode<'q, Postgres> for PgCircle { } impl FromStr for PgCircle { - type Err = Error; + type Err = BoxDynError; fn from_str(s: &str) -> Result { let sanitised = s.replace(['<', '>', '(', ')', ' '], ""); @@ -72,23 +72,17 @@ impl FromStr for PgCircle { let x = parts .next() .and_then(|s| s.trim().parse::().ok()) - .ok_or(Error::Decode( - format!("{}: could not get x from {}", ERROR, s).into(), - ))?; + .ok_or_else(|| format!("{}: could not get x from {}", ERROR, s))?; let y = parts .next() .and_then(|s| s.trim().parse::().ok()) - .ok_or(Error::Decode( - format!("{}: could not get y from {}", ERROR, s).into(), - ))?; + .ok_or_else(|| format!("{}: could not get y from {}", ERROR, s))?; let r = parts .next() .and_then(|s| s.trim().parse::().ok()) - .ok_or(Error::Decode( - format!("{}: could not get r from {}", ERROR, s).into(), - ))?; + .ok_or_else(|| format!("{}: could not get r from {}", ERROR, s))?; Ok(PgCircle { x, y, r }) } diff --git a/sqlx-postgres/src/types/geometry/line.rs b/sqlx-postgres/src/types/geometry/line.rs index daa487bf45..a033b87a92 100644 --- a/sqlx-postgres/src/types/geometry/line.rs +++ b/sqlx-postgres/src/types/geometry/line.rs @@ -57,7 +57,7 @@ impl<'q> Encode<'q, Postgres> for PgLine { } impl FromStr for PgLine { - type Err = Error; + type Err = BoxDynError; fn from_str(s: &str) -> Result { let mut parts = s @@ -67,23 +67,17 @@ impl FromStr for PgLine { let a = parts .next() .and_then(|s| s.trim().parse::().ok()) - .ok_or(Error::Decode( - format!("{}: could not get a from {}", ERROR, s).into(), - ))?; + .ok_or_else(|| format!("{}: could not get a from {}", ERROR, s))?; let b = parts .next() .and_then(|s| s.trim().parse::().ok()) - .ok_or(Error::Decode( - format!("{}: could not get b from {}", ERROR, s).into(), - ))?; + .ok_or_else(|| format!("{}: could not get b from {}", ERROR, s))?; let c = parts .next() .and_then(|s| s.trim().parse::().ok()) - .ok_or(Error::Decode( - format!("{}: could not get c from {}", ERROR, s).into(), - ))?; + .ok_or_else(|| format!("{}: could not get c from {}", ERROR, s))?; Ok(PgLine { a, b, c }) } diff --git a/sqlx-postgres/src/types/geometry/line_segment.rs b/sqlx-postgres/src/types/geometry/line_segment.rs index b4ab0d30ac..a4d0f89c0b 100644 --- a/sqlx-postgres/src/types/geometry/line_segment.rs +++ b/sqlx-postgres/src/types/geometry/line_segment.rs @@ -66,7 +66,7 @@ impl<'q> Encode<'q, Postgres> for PgLSeg { } impl FromStr for PgLSeg { - type Err = Error; + type Err = BoxDynError; fn from_str(s: &str) -> Result { let sanitised = s.replace(['(', ')', '[', ']', ' '], ""); @@ -75,30 +75,22 @@ impl FromStr for PgLSeg { let x1 = parts .next() .and_then(|s| s.parse::().ok()) - .ok_or(Error::Decode( - format!("{}: could not get x1 from {}", ERROR, s).into(), - ))?; + .ok_or_else(|| format!("{}: could not get x1 from {}", ERROR, s))?; let y1 = parts .next() .and_then(|s| s.parse::().ok()) - .ok_or(Error::Decode( - format!("{}: could not get y1 from {}", ERROR, s).into(), - ))?; + .ok_or_else(|| format!("{}: could not get y1 from {}", ERROR, s))?; let x2 = parts .next() .and_then(|s| s.parse::().ok()) - .ok_or(Error::Decode( - format!("{}: could not get x2 from {}", ERROR, s).into(), - ))?; + .ok_or_else(|| format!("{}: could not get x2 from {}", ERROR, s))?; let y2 = parts .next() .and_then(|s| s.parse::().ok()) - .ok_or(Error::Decode( - format!("{}: could not get y2 from {}", ERROR, s).into(), - ))?; + .ok_or_else(|| format!("{}: could not get y2 from {}", ERROR, s))?; Ok(PgLSeg { x1, y1, x2, y2 }) } diff --git a/sqlx-postgres/src/types/geometry/path.rs b/sqlx-postgres/src/types/geometry/path.rs index a8709d576c..b4aafdd9ed 100644 --- a/sqlx-postgres/src/types/geometry/path.rs +++ b/sqlx-postgres/src/types/geometry/path.rs @@ -308,7 +308,7 @@ mod path_tests { if let Err(err) = path { assert_eq!( err.to_string(), - format!("error occurred while decoding: Unmatched pair in path: {input_str}") + format!("error occurred while decoding: Unmatched pair in PATH: {input_str}") ) } } diff --git a/sqlx-postgres/src/types/geometry/point.rs b/sqlx-postgres/src/types/geometry/point.rs index 028a97b6f6..cc10672950 100644 --- a/sqlx-postgres/src/types/geometry/point.rs +++ b/sqlx-postgres/src/types/geometry/point.rs @@ -65,15 +65,13 @@ fn parse_float_from_str(s: &str, error_msg: &str) -> Result { } impl FromStr for PgPoint { - type Err = Error; + type Err = BoxDynError; fn from_str(s: &str) -> Result { let (x_str, y_str) = s .trim_matches(|c| c == '(' || c == ')' || c == ' ') .split_once(',') - .ok_or(Error::Decode( - format!("error decoding POINT: could not get x and y from {}", s).into(), - ))?; + .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")?; From d559b0783511e52640c0db795ef231113ce8e0cf Mon Sep 17 00:00:00 2001 From: James Holman Date: Sun, 8 Sep 2024 13:22:03 +1000 Subject: [PATCH 50/64] fix: remove unused error imports --- sqlx-postgres/src/types/geometry/box.rs | 1 - sqlx-postgres/src/types/geometry/line.rs | 1 - sqlx-postgres/src/types/geometry/line_segment.rs | 1 - 3 files changed, 3 deletions(-) diff --git a/sqlx-postgres/src/types/geometry/box.rs b/sqlx-postgres/src/types/geometry/box.rs index 32316bb7e4..92493cd960 100644 --- a/sqlx-postgres/src/types/geometry/box.rs +++ b/sqlx-postgres/src/types/geometry/box.rs @@ -4,7 +4,6 @@ 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; const ERROR: &str = "error decoding BOX"; diff --git a/sqlx-postgres/src/types/geometry/line.rs b/sqlx-postgres/src/types/geometry/line.rs index a033b87a92..7180241cb0 100644 --- a/sqlx-postgres/src/types/geometry/line.rs +++ b/sqlx-postgres/src/types/geometry/line.rs @@ -4,7 +4,6 @@ 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; const ERROR: &str = "error decoding LINE"; diff --git a/sqlx-postgres/src/types/geometry/line_segment.rs b/sqlx-postgres/src/types/geometry/line_segment.rs index a4d0f89c0b..9e55d334b3 100644 --- a/sqlx-postgres/src/types/geometry/line_segment.rs +++ b/sqlx-postgres/src/types/geometry/line_segment.rs @@ -4,7 +4,6 @@ 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; const ERROR: &str = "error decoding LSEG"; From a3c25f301d451526c7eeb3b154e0d07ae372017e Mon Sep 17 00:00:00 2001 From: James Holman Date: Sun, 8 Sep 2024 13:24:04 +1000 Subject: [PATCH 51/64] fix: radius variable circle --- sqlx-postgres/src/types/geometry/circle.rs | 34 +++++++++++----------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/sqlx-postgres/src/types/geometry/circle.rs b/sqlx-postgres/src/types/geometry/circle.rs index 446505d8ab..b9364b2df3 100644 --- a/sqlx-postgres/src/types/geometry/circle.rs +++ b/sqlx-postgres/src/types/geometry/circle.rs @@ -12,22 +12,22 @@ const ERROR: &str = "error decoding CIRCLE"; /// ## Postgres Geometric Circle type /// /// Description: Circle -/// Representation: `< (x, y), r >` (center point and radius) +/// Representation: `< (x, y), radius >` (center point and radius) /// /// ```text -/// < ( x , y ) , r > -/// ( ( x , y ) , r ) -/// ( x , y ) , r -/// x , y , r +/// < ( x , y ) , radius > +/// ( ( x , y ) , radius ) +/// ( x , y ) , radius +/// x , y , radius /// ``` -/// where `(x,y)` is the center point and r is the radius of the circle. +/// where `(x,y)` is the center point. /// /// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-CIRCLE #[derive(Debug, Clone, PartialEq)] pub struct PgCircle { pub x: f64, pub y: f64, - pub r: f64, + pub radius: f64, } impl Type for PgCircle { @@ -84,7 +84,7 @@ impl FromStr for PgCircle { .and_then(|s| s.trim().parse::().ok()) .ok_or_else(|| format!("{}: could not get r from {}", ERROR, s))?; - Ok(PgCircle { x, y, r }) + Ok(PgCircle { x, y, radius: r }) } } @@ -93,13 +93,13 @@ impl PgCircle { let x = bytes.get_f64(); let y = bytes.get_f64(); let r = bytes.get_f64(); - Ok(PgCircle { x, y, r }) + Ok(PgCircle { x, y, radius: r }) } fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), Error> { buff.extend_from_slice(&self.x.to_be_bytes()); buff.extend_from_slice(&self.y.to_be_bytes()); - buff.extend_from_slice(&self.r.to_be_bytes()); + buff.extend_from_slice(&self.radius.to_be_bytes()); Ok(()) } @@ -131,7 +131,7 @@ mod circle_tests { PgCircle { x: 1.1, y: 2.2, - r: 3.3 + radius: 3.3 } ) } @@ -144,7 +144,7 @@ mod circle_tests { PgCircle { x: 1.0, y: 2.0, - r: 3.0 + radius: 3.0 } ); } @@ -157,7 +157,7 @@ mod circle_tests { PgCircle { x: 1.0, y: 2.0, - r: 3.0 + radius: 3.0 } ); } @@ -170,7 +170,7 @@ mod circle_tests { PgCircle { x: 1.0, y: 2.0, - r: 3.0 + radius: 3.0 } ); } @@ -183,7 +183,7 @@ mod circle_tests { PgCircle { x: 1.0, y: 2.0, - r: 3.0 + radius: 3.0 } ); } @@ -196,7 +196,7 @@ mod circle_tests { PgCircle { x: 1.1, y: 2.2, - r: 3.3 + radius: 3.3 } ); } @@ -206,7 +206,7 @@ mod circle_tests { let circle = PgCircle { x: 1.1, y: 2.2, - r: 3.3, + radius: 3.3, }; assert_eq!(circle.serialize_to_vec(), CIRCLE_BYTES,) } From 5a9a6f6ca58aa637efa894b3d2b29a4356ccece2 Mon Sep 17 00:00:00 2001 From: James Holman Date: Sun, 8 Sep 2024 13:25:17 +1000 Subject: [PATCH 52/64] fix: path import --- sqlx-postgres/src/types/geometry/path.rs | 2 +- sqlx-postgres/src/types/geometry/polygon.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sqlx-postgres/src/types/geometry/path.rs b/sqlx-postgres/src/types/geometry/path.rs index b4aafdd9ed..27861c6a6b 100644 --- a/sqlx-postgres/src/types/geometry/path.rs +++ b/sqlx-postgres/src/types/geometry/path.rs @@ -172,7 +172,7 @@ impl PgPath { } impl Header { - const HEADER_WIDTH: usize = size_of::() + size_of::(); + const HEADER_WIDTH: usize = mem::size_of::() + mem::size_of::(); fn data_size(&self) -> usize { self.length * BYTE_WIDTH * 2 diff --git a/sqlx-postgres/src/types/geometry/polygon.rs b/sqlx-postgres/src/types/geometry/polygon.rs index fecc0f4f17..6a4c812108 100644 --- a/sqlx-postgres/src/types/geometry/polygon.rs +++ b/sqlx-postgres/src/types/geometry/polygon.rs @@ -165,7 +165,7 @@ impl PgPolygon { } impl Header { - const HEADER_WIDTH: usize = size_of::() + size_of::(); + const HEADER_WIDTH: usize = mem::size_of::() + mem::size_of::(); fn data_size(&self) -> usize { self.length * BYTE_WIDTH * 2 From 815c22cd2b9eca7ad97cae7992049c5e7a6f5736 Mon Sep 17 00:00:00 2001 From: James Holman Date: Sun, 8 Sep 2024 13:31:09 +1000 Subject: [PATCH 53/64] fix: splitn str --- sqlx-postgres/src/types/geometry/box.rs | 17 ++++++++++++++++- .../src/types/geometry/line_segment.rs | 2 +- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/sqlx-postgres/src/types/geometry/box.rs b/sqlx-postgres/src/types/geometry/box.rs index 92493cd960..48e74bcc6f 100644 --- a/sqlx-postgres/src/types/geometry/box.rs +++ b/sqlx-postgres/src/types/geometry/box.rs @@ -69,7 +69,7 @@ impl FromStr for PgBox { fn from_str(s: &str) -> Result { let sanitised = s.replace(['(', ')', '[', ']', ' '], ""); - let mut parts = sanitised.splitn(4, ","); + let mut parts = sanitised.splitn(4, ','); let x1 = parts .next() @@ -208,6 +208,21 @@ mod box_tests { ); } + #[test] + fn can_deserialise_too_many_numbers() { + let input_str = "1, 2, 3, 4, 5"; + let pg_box = PgBox::from_str(input_str); + + assert!(pg_box.is_err()); + + if let Err(err) = pg_box { + assert_eq!( + err.to_string(), + format!("error decoding BOX: could not get y2 from 1, 2, 3, 4, 5") + ) + } + } + #[test] fn can_deserialise_box_type_str_float() { let pg_box = PgBox::from_str("(1.1, 2.2), (3.3, 4.4)").unwrap(); diff --git a/sqlx-postgres/src/types/geometry/line_segment.rs b/sqlx-postgres/src/types/geometry/line_segment.rs index 9e55d334b3..9adfaa1e8b 100644 --- a/sqlx-postgres/src/types/geometry/line_segment.rs +++ b/sqlx-postgres/src/types/geometry/line_segment.rs @@ -69,7 +69,7 @@ impl FromStr for PgLSeg { fn from_str(s: &str) -> Result { let sanitised = s.replace(['(', ')', '[', ']', ' '], ""); - let mut parts = sanitised.splitn(4, ","); + let mut parts = sanitised.splitn(4, ','); let x1 = parts .next() From 0b49de39a93f5b71f5d28326f3f4b5358adf92fb Mon Sep 17 00:00:00 2001 From: James Holman Date: Sun, 8 Sep 2024 13:48:58 +1000 Subject: [PATCH 54/64] test: more circle tests --- sqlx-postgres/src/types/geometry/box.rs | 34 ++++++++++++-- sqlx-postgres/src/types/geometry/circle.rs | 36 +++++++++++++-- .../src/types/geometry/line_segment.rs | 45 ++++++++++++++++++- 3 files changed, 108 insertions(+), 7 deletions(-) diff --git a/sqlx-postgres/src/types/geometry/box.rs b/sqlx-postgres/src/types/geometry/box.rs index 48e74bcc6f..b96efdb66c 100644 --- a/sqlx-postgres/src/types/geometry/box.rs +++ b/sqlx-postgres/src/types/geometry/box.rs @@ -69,7 +69,7 @@ impl FromStr for PgBox { fn from_str(s: &str) -> Result { let sanitised = s.replace(['(', ')', '[', ']', ' '], ""); - let mut parts = sanitised.splitn(4, ','); + let mut parts = sanitised.split(','); let x1 = parts .next() @@ -91,6 +91,10 @@ impl FromStr for PgBox { .and_then(|s| s.parse::().ok()) .ok_or_else(|| format!("{}: could not get y2 from {}", ERROR, s))?; + if parts.next().is_some() { + return Err(format!("{}: too many points in {}", ERROR, s).into()); + } + Ok(PgBox { x1, y1, x2, y2 }) } } @@ -209,16 +213,40 @@ mod box_tests { } #[test] - fn can_deserialise_too_many_numbers() { + fn cannot_deserialise_too_many_numbers() { let input_str = "1, 2, 3, 4, 5"; let pg_box = PgBox::from_str(input_str); + assert!(pg_box.is_err()); + if let Err(err) = pg_box { + assert_eq!( + err.to_string(), + format!("error decoding BOX: too many points in {input_str}") + ) + } + } + #[test] + fn cannot_deserialise_too_few_numbers() { + let input_str = "1, 2, 3 "; + let pg_box = PgBox::from_str(input_str); assert!(pg_box.is_err()); + if let Err(err) = pg_box { + assert_eq!( + err.to_string(), + format!("error decoding BOX: could not get y2 from {input_str}") + ) + } + } + #[test] + fn cannot_deserialise_invalid_numbers() { + let input_str = "1, 2, 3, FOUR"; + let pg_box = PgBox::from_str(input_str); + assert!(pg_box.is_err()); if let Err(err) = pg_box { assert_eq!( err.to_string(), - format!("error decoding BOX: could not get y2 from 1, 2, 3, 4, 5") + format!("error decoding BOX: could not get y2 from {input_str}") ) } } diff --git a/sqlx-postgres/src/types/geometry/circle.rs b/sqlx-postgres/src/types/geometry/circle.rs index b9364b2df3..eb37a92776 100644 --- a/sqlx-postgres/src/types/geometry/circle.rs +++ b/sqlx-postgres/src/types/geometry/circle.rs @@ -79,12 +79,16 @@ impl FromStr for PgCircle { .and_then(|s| s.trim().parse::().ok()) .ok_or_else(|| format!("{}: could not get y from {}", ERROR, s))?; - let r = parts + let radius = parts .next() .and_then(|s| s.trim().parse::().ok()) - .ok_or_else(|| format!("{}: could not get r from {}", ERROR, s))?; + .ok_or_else(|| format!("{}: could not get radius from {}", ERROR, s))?; - Ok(PgCircle { x, y, radius: r }) + if radius < 0. { + return Err(format!("{}: cannot have negative radius: {}", ERROR, s).into()); + } + + Ok(PgCircle { x, y, radius }) } } @@ -188,6 +192,32 @@ mod circle_tests { ); } + #[test] + fn cannot_deserialise_circle_invalid_numbers() { + let input_str = "1, 2, Three"; + let circle = PgCircle::from_str(input_str); + assert!(circle.is_err()); + if let Err(err) = circle { + assert_eq!( + err.to_string(), + format!("error decoding CIRCLE: could not get radius from {input_str}") + ) + } + } + + #[test] + fn cannot_deserialise_circle_negative_radius() { + let input_str = "1, 2, -3"; + let circle = PgCircle::from_str(input_str); + assert!(circle.is_err()); + if let Err(err) = circle { + assert_eq!( + err.to_string(), + format!("error decoding CIRCLE: cannot have negative radius: {input_str}") + ) + } + } + #[test] fn can_deserialise_circle_type_str_float() { let circle = PgCircle::from_str("<(1.1, 2.2), 3.3>").unwrap(); diff --git a/sqlx-postgres/src/types/geometry/line_segment.rs b/sqlx-postgres/src/types/geometry/line_segment.rs index 9adfaa1e8b..63207f061a 100644 --- a/sqlx-postgres/src/types/geometry/line_segment.rs +++ b/sqlx-postgres/src/types/geometry/line_segment.rs @@ -69,7 +69,7 @@ impl FromStr for PgLSeg { fn from_str(s: &str) -> Result { let sanitised = s.replace(['(', ')', '[', ']', ' '], ""); - let mut parts = sanitised.splitn(4, ','); + let mut parts = sanitised.split(','); let x1 = parts .next() @@ -91,6 +91,10 @@ impl FromStr for PgLSeg { .and_then(|s| s.parse::().ok()) .ok_or_else(|| format!("{}: could not get y2 from {}", ERROR, s))?; + if parts.next().is_some() { + return Err(format!("{}: too many points in {}", ERROR, s).into()); + } + Ok(PgLSeg { x1, y1, x2, y2 }) } } @@ -202,6 +206,45 @@ mod lseg_tests { ); } + #[test] + fn can_deserialise_too_many_numbers() { + let input_str = "1, 2, 3, 4, 5"; + let pg_box = PgLSeg::from_str(input_str); + assert!(pg_box.is_err()); + if let Err(err) = pg_box { + assert_eq!( + err.to_string(), + format!("error decoding LSEG: too many points in {input_str}") + ) + } + } + + #[test] + fn can_deserialise_too_few_numbers() { + let input_str = "1, 2, 3"; + let pg_box = PgLSeg::from_str(input_str); + assert!(pg_box.is_err()); + if let Err(err) = pg_box { + assert_eq!( + err.to_string(), + format!("error decoding LSEG: could not get y2 from {input_str}") + ) + } + } + + #[test] + fn can_deserialise_invalid_numbers() { + let input_str = "1, 2, 3, FOUR"; + let pg_box = PgLSeg::from_str(input_str); + assert!(pg_box.is_err()); + if let Err(err) = pg_box { + assert_eq!( + err.to_string(), + format!("error decoding LSEG: could not get y2 from {input_str}") + ) + } + } + #[test] fn can_deserialise_lseg_type_str_float() { let lseg = PgLSeg::from_str("(1.1, 2.2), (3.3, 4.4)").unwrap(); From bf0d599fbaa278ce5af3c81a774e4b949ba6e529 Mon Sep 17 00:00:00 2001 From: James Holman Date: Sun, 8 Sep 2024 13:52:43 +1000 Subject: [PATCH 55/64] test: lines --- sqlx-postgres/src/types/geometry/line.rs | 39 +++++++++++++++++++ .../src/types/geometry/line_segment.rs | 18 ++++----- 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/sqlx-postgres/src/types/geometry/line.rs b/sqlx-postgres/src/types/geometry/line.rs index 7180241cb0..b03bee2b6f 100644 --- a/sqlx-postgres/src/types/geometry/line.rs +++ b/sqlx-postgres/src/types/geometry/line.rs @@ -143,6 +143,45 @@ mod line_tests { ); } + #[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: could not get c from {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(); diff --git a/sqlx-postgres/src/types/geometry/line_segment.rs b/sqlx-postgres/src/types/geometry/line_segment.rs index 63207f061a..de284cb693 100644 --- a/sqlx-postgres/src/types/geometry/line_segment.rs +++ b/sqlx-postgres/src/types/geometry/line_segment.rs @@ -209,9 +209,9 @@ mod lseg_tests { #[test] fn can_deserialise_too_many_numbers() { let input_str = "1, 2, 3, 4, 5"; - let pg_box = PgLSeg::from_str(input_str); - assert!(pg_box.is_err()); - if let Err(err) = pg_box { + let lseg = PgLSeg::from_str(input_str); + assert!(lseg.is_err()); + if let Err(err) = lseg { assert_eq!( err.to_string(), format!("error decoding LSEG: too many points in {input_str}") @@ -222,9 +222,9 @@ mod lseg_tests { #[test] fn can_deserialise_too_few_numbers() { let input_str = "1, 2, 3"; - let pg_box = PgLSeg::from_str(input_str); - assert!(pg_box.is_err()); - if let Err(err) = pg_box { + let lseg = PgLSeg::from_str(input_str); + assert!(lseg.is_err()); + if let Err(err) = lseg { assert_eq!( err.to_string(), format!("error decoding LSEG: could not get y2 from {input_str}") @@ -235,9 +235,9 @@ mod lseg_tests { #[test] fn can_deserialise_invalid_numbers() { let input_str = "1, 2, 3, FOUR"; - let pg_box = PgLSeg::from_str(input_str); - assert!(pg_box.is_err()); - if let Err(err) = pg_box { + let lseg = PgLSeg::from_str(input_str); + assert!(lseg.is_err()); + if let Err(err) = lseg { assert_eq!( err.to_string(), format!("error decoding LSEG: could not get y2 from {input_str}") From 782005e51f3a9264ea8c39d4cba165ff095fa183 Mon Sep 17 00:00:00 2001 From: James Holman Date: Sun, 8 Sep 2024 13:57:28 +1000 Subject: [PATCH 56/64] fix: splits --- sqlx-postgres/src/types/geometry/path.rs | 2 +- sqlx-postgres/src/types/geometry/polygon.rs | 33 ++++++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/sqlx-postgres/src/types/geometry/path.rs b/sqlx-postgres/src/types/geometry/path.rs index 27861c6a6b..ad93e5b51b 100644 --- a/sqlx-postgres/src/types/geometry/path.rs +++ b/sqlx-postgres/src/types/geometry/path.rs @@ -78,7 +78,7 @@ impl FromStr for PgPath { fn from_str(s: &str) -> Result { let closed = !s.contains("["); let sanitised = s.replace(['(', ')', '[', ']', ' '], ""); - let parts = sanitised.split(",").collect::>(); + let parts = sanitised.split(',').collect::>(); let mut points = vec![]; diff --git a/sqlx-postgres/src/types/geometry/polygon.rs b/sqlx-postgres/src/types/geometry/polygon.rs index 6a4c812108..500c9933e9 100644 --- a/sqlx-postgres/src/types/geometry/polygon.rs +++ b/sqlx-postgres/src/types/geometry/polygon.rs @@ -76,7 +76,7 @@ impl FromStr for PgPolygon { fn from_str(s: &str) -> Result { let sanitised = s.replace(['(', ')', '[', ']', ' '], ""); - let parts = sanitised.split(",").collect::>(); + let parts = sanitised.split(',').collect::>(); let mut points = vec![]; @@ -265,6 +265,7 @@ mod polygon_tests { } ); } + #[test] fn can_deserialise_polygon_type_str_second_syntax() { let polygon = PgPolygon::from_str("(( 1, 2), (3, 4 ))").unwrap(); @@ -276,6 +277,36 @@ mod polygon_tests { ); } + #[test] + fn cannot_deserialise_polygon_type_str_uneven_points_first_syntax() { + let input_str = "[( 1, 2), (3)]"; + let polygon = PgPolygon::from_str(input_str); + + assert!(polygon.is_err()); + + if let Err(err) = polygon { + assert_eq!( + err.to_string(), + format!("error occurred while decoding: Unmatched pair in POLYGON: {input_str}") + ) + } + } + + #[test] + fn cannot_deserialise_polygon_type_str_invalid_numbers() { + let input_str = "[( 1, 2), (2, three)]"; + let polygon = PgPolygon::from_str(input_str); + + assert!(polygon.is_err()); + + if let Err(err) = polygon { + assert_eq!( + err.to_string(), + format!("error occurred while decoding: could not get y") + ) + } + } + #[test] fn can_deserialise_polygon_type_str_third_syntax() { let polygon = PgPolygon::from_str("(1, 2), (3, 4 )").unwrap(); From e343924f9983ad3ed8db760815d53955cb60c8fe Mon Sep 17 00:00:00 2001 From: James Holman Date: Sun, 8 Sep 2024 13:58:18 +1000 Subject: [PATCH 57/64] fix: splits --- sqlx-postgres/src/types/geometry/path.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlx-postgres/src/types/geometry/path.rs b/sqlx-postgres/src/types/geometry/path.rs index ad93e5b51b..87a3b3e8d3 100644 --- a/sqlx-postgres/src/types/geometry/path.rs +++ b/sqlx-postgres/src/types/geometry/path.rs @@ -76,7 +76,7 @@ impl FromStr for PgPath { type Err = Error; fn from_str(s: &str) -> Result { - let closed = !s.contains("["); + let closed = !s.contains('['); let sanitised = s.replace(['(', ')', '[', ']', ' '], ""); let parts = sanitised.split(',').collect::>(); From 79f18f18a8c13e88d04b5e9782f3a6895782dd90 Mon Sep 17 00:00:00 2001 From: James Holman Date: Sun, 8 Sep 2024 14:01:19 +1000 Subject: [PATCH 58/64] fix: lower upper with box --- sqlx-postgres/src/types/geometry/box.rs | 150 +++++++++++++----------- 1 file changed, 80 insertions(+), 70 deletions(-) diff --git a/sqlx-postgres/src/types/geometry/box.rs b/sqlx-postgres/src/types/geometry/box.rs index b96efdb66c..e24d7940b5 100644 --- a/sqlx-postgres/src/types/geometry/box.rs +++ b/sqlx-postgres/src/types/geometry/box.rs @@ -11,25 +11,25 @@ const ERROR: &str = "error decoding BOX"; /// ## Postgres Geometric Box type /// /// Description: Rectangular box -/// Representation: `((x1,y1),(x2,y2))` +/// Representation: `((x_upper_right,y_upper_right),(x_lower_left,y_lower_left))` /// /// Boxes are represented by pairs of points that are opposite corners of the box. Values of type box are specified using any of the following syntaxes: /// /// ```text -/// ( ( x1 , y1 ) , ( x2 , y2 ) ) -/// ( x1 , y1 ) , ( x2 , y2 ) -/// x1 , y1 , x2 , y2 +/// ( ( x_upper_right , y_upper_right ) , ( x_lower_left , y_lower_left ) ) +/// ( x_upper_right , y_upper_right ) , ( x_lower_left , y_lower_left ) +/// x_upper_right , y_upper_right , x_lower_left , y_lower_left /// ``` -/// where `(x1,y1) and (x2,y2)` are any two opposite corners of the box. +/// where `(x_upper_right,y_upper_right) and (x_lower_left,y_lower_left)` are any two opposite corners of the box. /// Any two opposite corners can be supplied on input, but the values will be reordered as needed to store the upper right and lower left corners, in that order. /// /// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-GEOMETRIC-BOXES #[derive(Debug, Clone, PartialEq)] pub struct PgBox { - pub x1: f64, - pub y1: f64, - pub x2: f64, - pub y2: f64, + pub x_upper_right: f64, + pub y_upper_right: f64, + pub x_lower_left: f64, + pub y_lower_left: f64, } impl Type for PgBox { @@ -71,49 +71,59 @@ impl FromStr for PgBox { let sanitised = s.replace(['(', ')', '[', ']', ' '], ""); let mut parts = sanitised.split(','); - let x1 = parts + let x_upper_right = parts .next() .and_then(|s| s.parse::().ok()) - .ok_or_else(|| format!("{}: could not get x1 from {}", ERROR, s))?; + .ok_or_else(|| format!("{}: could not get x_upper_right from {}", ERROR, s))?; - let y1 = parts + let y_upper_right = parts .next() .and_then(|s| s.parse::().ok()) - .ok_or_else(|| format!("{}: could not get y1 from {}", ERROR, s))?; + .ok_or_else(|| format!("{}: could not get y_upper_right from {}", ERROR, s))?; - let x2 = parts + let x_lower_left = parts .next() .and_then(|s| s.parse::().ok()) - .ok_or_else(|| format!("{}: could not get x2 from {}", ERROR, s))?; + .ok_or_else(|| format!("{}: could not get x_lower_left from {}", ERROR, s))?; - let y2 = parts + let y_lower_left = parts .next() .and_then(|s| s.parse::().ok()) - .ok_or_else(|| format!("{}: could not get y2 from {}", ERROR, s))?; + .ok_or_else(|| format!("{}: could not get y_lower_left from {}", ERROR, s))?; if parts.next().is_some() { return Err(format!("{}: too many points in {}", ERROR, s).into()); } - Ok(PgBox { x1, y1, x2, y2 }) + Ok(PgBox { + x_upper_right, + y_upper_right, + x_lower_left, + y_lower_left, + }) } } impl PgBox { fn from_bytes(mut bytes: &[u8]) -> Result { - let x1 = bytes.get_f64(); - let y1 = bytes.get_f64(); - let x2 = bytes.get_f64(); - let y2 = bytes.get_f64(); - - Ok(PgBox { x1, y1, x2, y2 }) + let x_upper_right = bytes.get_f64(); + let y_upper_right = bytes.get_f64(); + let x_lower_left = bytes.get_f64(); + let y_lower_left = bytes.get_f64(); + + Ok(PgBox { + x_upper_right, + y_upper_right, + x_lower_left, + y_lower_left, + }) } fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), String> { - let min_x = &self.x1.min(self.x2); - let min_y = &self.y1.min(self.y2); - let max_x = &self.x1.max(self.x2); - let max_y = &self.y1.max(self.y2); + let min_x = &self.x_upper_right.min(self.x_lower_left); + let min_y = &self.y_upper_right.min(self.y_lower_left); + let max_x = &self.x_upper_right.max(self.x_lower_left); + let max_y = &self.y_upper_right.max(self.y_lower_left); buff.extend_from_slice(&max_x.to_be_bytes()); buff.extend_from_slice(&max_y.to_be_bytes()); @@ -149,10 +159,10 @@ mod box_tests { assert_eq!( pg_box, PgBox { - x1: 2., - y1: 2., - x2: -2., - y2: -2. + x_upper_right: 2., + y_upper_right: 2., + x_lower_left: -2., + y_lower_left: -2. } ) } @@ -163,10 +173,10 @@ mod box_tests { assert_eq!( pg_box, PgBox { - x1: 1., - y1: 2., - x2: 3., - y2: 4. + x_upper_right: 1., + y_upper_right: 2., + x_lower_left: 3., + y_lower_left: 4. } ); } @@ -176,10 +186,10 @@ mod box_tests { assert_eq!( pg_box, PgBox { - x1: 1., - y1: 2., - x2: 3., - y2: 4. + x_upper_right: 1., + y_upper_right: 2., + x_lower_left: 3., + y_lower_left: 4. } ); } @@ -190,10 +200,10 @@ mod box_tests { assert_eq!( pg_box, PgBox { - x1: 1., - y1: 2., - x2: 3., - y2: 4. + x_upper_right: 1., + y_upper_right: 2., + x_lower_left: 3., + y_lower_left: 4. } ); } @@ -204,10 +214,10 @@ mod box_tests { assert_eq!( pg_box, PgBox { - x1: 1., - y1: 2., - x2: 3., - y2: 4. + x_upper_right: 1., + y_upper_right: 2., + x_lower_left: 3., + y_lower_left: 4. } ); } @@ -233,7 +243,7 @@ mod box_tests { if let Err(err) = pg_box { assert_eq!( err.to_string(), - format!("error decoding BOX: could not get y2 from {input_str}") + format!("error decoding BOX: could not get y_lower_left from {input_str}") ) } } @@ -246,7 +256,7 @@ mod box_tests { if let Err(err) = pg_box { assert_eq!( err.to_string(), - format!("error decoding BOX: could not get y2 from {input_str}") + format!("error decoding BOX: could not get y_lower_left from {input_str}") ) } } @@ -257,10 +267,10 @@ mod box_tests { assert_eq!( pg_box, PgBox { - x1: 1.1, - y1: 2.2, - x2: 3.3, - y2: 4.4 + x_upper_right: 1.1, + y_upper_right: 2.2, + x_lower_left: 3.3, + y_lower_left: 4.4 } ); } @@ -268,10 +278,10 @@ mod box_tests { #[test] fn can_serialise_box_type_in_order() { let pg_box = PgBox { - x1: 2., - x2: -2., - y1: -2., - y2: 2., + x_upper_right: 2., + x_lower_left: -2., + y_upper_right: -2., + y_lower_left: 2., }; assert_eq!(pg_box.serialize_to_vec(), BOX_BYTES,) } @@ -279,10 +289,10 @@ mod box_tests { #[test] fn can_serialise_box_type_out_of_order() { let pg_box = PgBox { - x1: -2., - x2: 2., - y1: 2., - y2: -2., + x_upper_right: -2., + x_lower_left: 2., + y_upper_right: 2., + y_lower_left: -2., }; assert_eq!(pg_box.serialize_to_vec(), BOX_BYTES,) } @@ -290,10 +300,10 @@ mod box_tests { #[test] fn can_order_box() { let pg_box = PgBox { - x1: -2., - x2: 2., - y1: 2., - y2: -2., + x_upper_right: -2., + x_lower_left: 2., + y_upper_right: 2., + y_lower_left: -2., }; let bytes = pg_box.serialize_to_vec(); @@ -301,10 +311,10 @@ mod box_tests { assert_eq!( pg_box, PgBox { - x1: 2., - y1: 2., - x2: -2., - y2: -2. + x_upper_right: 2., + y_upper_right: 2., + x_lower_left: -2., + y_lower_left: -2. } ) } From e67b19505e3252b8bd1071064c8b578d9ae71437 Mon Sep 17 00:00:00 2001 From: James Holman Date: Sun, 8 Sep 2024 14:02:47 +1000 Subject: [PATCH 59/64] fix: circle too many points --- sqlx-postgres/src/types/geometry/circle.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sqlx-postgres/src/types/geometry/circle.rs b/sqlx-postgres/src/types/geometry/circle.rs index eb37a92776..35ad0412f1 100644 --- a/sqlx-postgres/src/types/geometry/circle.rs +++ b/sqlx-postgres/src/types/geometry/circle.rs @@ -67,7 +67,7 @@ impl FromStr for PgCircle { fn from_str(s: &str) -> Result { let sanitised = s.replace(['<', '>', '(', ')', ' '], ""); - let mut parts = sanitised.splitn(3, ','); + let mut parts = sanitised.split(','); let x = parts .next() @@ -84,6 +84,10 @@ impl FromStr for PgCircle { .and_then(|s| s.trim().parse::().ok()) .ok_or_else(|| format!("{}: could not get radius from {}", ERROR, s))?; + if parts.next().is_some() { + return Err(format!("{}: too many points in {}", ERROR, s).into()); + } + if radius < 0. { return Err(format!("{}: cannot have negative radius: {}", ERROR, s).into()); } From bbda7d048db248692afcc5b26f777c99bc8bb689 Mon Sep 17 00:00:00 2001 From: James Holman Date: Sun, 8 Sep 2024 14:04:03 +1000 Subject: [PATCH 60/64] fix: line too many points --- sqlx-postgres/src/types/geometry/line.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sqlx-postgres/src/types/geometry/line.rs b/sqlx-postgres/src/types/geometry/line.rs index b03bee2b6f..6f3b4012de 100644 --- a/sqlx-postgres/src/types/geometry/line.rs +++ b/sqlx-postgres/src/types/geometry/line.rs @@ -61,7 +61,7 @@ impl FromStr for PgLine { fn from_str(s: &str) -> Result { let mut parts = s .trim_matches(|c| c == '{' || c == '}' || c == ' ') - .splitn(3, ','); + .split(','); let a = parts .next() @@ -78,6 +78,10 @@ impl FromStr for PgLine { .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 points in {}", ERROR, s).into()); + } + Ok(PgLine { a, b, c }) } } From 89fd0b02e77b8d8aceead3874e2ed44dee2f8d0e Mon Sep 17 00:00:00 2001 From: James Holman Date: Sun, 8 Sep 2024 14:05:38 +1000 Subject: [PATCH 61/64] fix: too many --- sqlx-postgres/src/types/geometry/box.rs | 4 ++-- sqlx-postgres/src/types/geometry/circle.rs | 2 +- sqlx-postgres/src/types/geometry/line.rs | 4 ++-- sqlx-postgres/src/types/geometry/line_segment.rs | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/sqlx-postgres/src/types/geometry/box.rs b/sqlx-postgres/src/types/geometry/box.rs index e24d7940b5..43d18a1f3d 100644 --- a/sqlx-postgres/src/types/geometry/box.rs +++ b/sqlx-postgres/src/types/geometry/box.rs @@ -92,7 +92,7 @@ impl FromStr for PgBox { .ok_or_else(|| format!("{}: could not get y_lower_left from {}", ERROR, s))?; if parts.next().is_some() { - return Err(format!("{}: too many points in {}", ERROR, s).into()); + return Err(format!("{}: too many numbers inputted in {}", ERROR, s).into()); } Ok(PgBox { @@ -230,7 +230,7 @@ mod box_tests { if let Err(err) = pg_box { assert_eq!( err.to_string(), - format!("error decoding BOX: too many points in {input_str}") + format!("error decoding BOX: too many numbers inputted in {input_str}") ) } } diff --git a/sqlx-postgres/src/types/geometry/circle.rs b/sqlx-postgres/src/types/geometry/circle.rs index 35ad0412f1..839505c35a 100644 --- a/sqlx-postgres/src/types/geometry/circle.rs +++ b/sqlx-postgres/src/types/geometry/circle.rs @@ -85,7 +85,7 @@ impl FromStr for PgCircle { .ok_or_else(|| format!("{}: could not get radius from {}", ERROR, s))?; if parts.next().is_some() { - return Err(format!("{}: too many points in {}", ERROR, s).into()); + return Err(format!("{}: too many numbers inputted in {}", ERROR, s).into()); } if radius < 0. { diff --git a/sqlx-postgres/src/types/geometry/line.rs b/sqlx-postgres/src/types/geometry/line.rs index 6f3b4012de..43f93c1c33 100644 --- a/sqlx-postgres/src/types/geometry/line.rs +++ b/sqlx-postgres/src/types/geometry/line.rs @@ -79,7 +79,7 @@ impl FromStr for PgLine { .ok_or_else(|| format!("{}: could not get c from {}", ERROR, s))?; if parts.next().is_some() { - return Err(format!("{}: too many points in {}", ERROR, s).into()); + return Err(format!("{}: too many numbers inputted in {}", ERROR, s).into()); } Ok(PgLine { a, b, c }) @@ -168,7 +168,7 @@ mod line_tests { if let Err(err) = line { assert_eq!( err.to_string(), - format!("error decoding LINE: could not get c from {input_str}") + format!("error decoding LINE: too many numbers inputted in {input_str}") ) } } diff --git a/sqlx-postgres/src/types/geometry/line_segment.rs b/sqlx-postgres/src/types/geometry/line_segment.rs index de284cb693..a168ae25a2 100644 --- a/sqlx-postgres/src/types/geometry/line_segment.rs +++ b/sqlx-postgres/src/types/geometry/line_segment.rs @@ -92,7 +92,7 @@ impl FromStr for PgLSeg { .ok_or_else(|| format!("{}: could not get y2 from {}", ERROR, s))?; if parts.next().is_some() { - return Err(format!("{}: too many points in {}", ERROR, s).into()); + return Err(format!("{}: too many numbers inputted in {}", ERROR, s).into()); } Ok(PgLSeg { x1, y1, x2, y2 }) @@ -214,7 +214,7 @@ mod lseg_tests { if let Err(err) = lseg { assert_eq!( err.to_string(), - format!("error decoding LSEG: too many points in {input_str}") + format!("error decoding LSEG: too many numbers inputted in {input_str}") ) } } From 896f44a2086ba78eb791f279ee88a3337fcbeed4 Mon Sep 17 00:00:00 2001 From: James Holman Date: Sun, 8 Sep 2024 14:12:24 +1000 Subject: [PATCH 62/64] fix: variable names for box and lseg --- sqlx-postgres/src/types/geometry/box.rs | 150 +++++++++--------- .../src/types/geometry/line_segment.rs | 128 ++++++++------- tests/postgres/types.rs | 8 +- 3 files changed, 148 insertions(+), 138 deletions(-) diff --git a/sqlx-postgres/src/types/geometry/box.rs b/sqlx-postgres/src/types/geometry/box.rs index 43d18a1f3d..988c028ed4 100644 --- a/sqlx-postgres/src/types/geometry/box.rs +++ b/sqlx-postgres/src/types/geometry/box.rs @@ -11,25 +11,25 @@ const ERROR: &str = "error decoding BOX"; /// ## Postgres Geometric Box type /// /// Description: Rectangular box -/// Representation: `((x_upper_right,y_upper_right),(x_lower_left,y_lower_left))` +/// Representation: `((upper_right_x,upper_right_y),(lower_left_x,lower_left_y))` /// /// Boxes are represented by pairs of points that are opposite corners of the box. Values of type box are specified using any of the following syntaxes: /// /// ```text -/// ( ( x_upper_right , y_upper_right ) , ( x_lower_left , y_lower_left ) ) -/// ( x_upper_right , y_upper_right ) , ( x_lower_left , y_lower_left ) -/// x_upper_right , y_upper_right , x_lower_left , y_lower_left +/// ( ( upper_right_x , upper_right_y ) , ( lower_left_x , lower_left_y ) ) +/// ( upper_right_x , upper_right_y ) , ( lower_left_x , lower_left_y ) +/// upper_right_x , upper_right_y , lower_left_x , lower_left_y /// ``` -/// where `(x_upper_right,y_upper_right) and (x_lower_left,y_lower_left)` are any two opposite corners of the box. +/// where `(upper_right_x,upper_right_y) and (lower_left_x,lower_left_y)` are any two opposite corners of the box. /// Any two opposite corners can be supplied on input, but the values will be reordered as needed to store the upper right and lower left corners, in that order. /// /// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-GEOMETRIC-BOXES #[derive(Debug, Clone, PartialEq)] pub struct PgBox { - pub x_upper_right: f64, - pub y_upper_right: f64, - pub x_lower_left: f64, - pub y_lower_left: f64, + pub upper_right_x: f64, + pub upper_right_y: f64, + pub lower_left_x: f64, + pub lower_left_y: f64, } impl Type for PgBox { @@ -71,59 +71,59 @@ impl FromStr for PgBox { let sanitised = s.replace(['(', ')', '[', ']', ' '], ""); let mut parts = sanitised.split(','); - let x_upper_right = parts + let upper_right_x = parts .next() .and_then(|s| s.parse::().ok()) - .ok_or_else(|| format!("{}: could not get x_upper_right from {}", ERROR, s))?; + .ok_or_else(|| format!("{}: could not get upper_right_x from {}", ERROR, s))?; - let y_upper_right = parts + let upper_right_y = parts .next() .and_then(|s| s.parse::().ok()) - .ok_or_else(|| format!("{}: could not get y_upper_right from {}", ERROR, s))?; + .ok_or_else(|| format!("{}: could not get upper_right_y from {}", ERROR, s))?; - let x_lower_left = parts + let lower_left_x = parts .next() .and_then(|s| s.parse::().ok()) - .ok_or_else(|| format!("{}: could not get x_lower_left from {}", ERROR, s))?; + .ok_or_else(|| format!("{}: could not get lower_left_x from {}", ERROR, s))?; - let y_lower_left = parts + let lower_left_y = parts .next() .and_then(|s| s.parse::().ok()) - .ok_or_else(|| format!("{}: could not get y_lower_left from {}", ERROR, s))?; + .ok_or_else(|| format!("{}: could not get lower_left_y from {}", ERROR, s))?; if parts.next().is_some() { return Err(format!("{}: too many numbers inputted in {}", ERROR, s).into()); } Ok(PgBox { - x_upper_right, - y_upper_right, - x_lower_left, - y_lower_left, + upper_right_x, + upper_right_y, + lower_left_x, + lower_left_y, }) } } impl PgBox { fn from_bytes(mut bytes: &[u8]) -> Result { - let x_upper_right = bytes.get_f64(); - let y_upper_right = bytes.get_f64(); - let x_lower_left = bytes.get_f64(); - let y_lower_left = bytes.get_f64(); + let upper_right_x = bytes.get_f64(); + let upper_right_y = bytes.get_f64(); + let lower_left_x = bytes.get_f64(); + let lower_left_y = bytes.get_f64(); Ok(PgBox { - x_upper_right, - y_upper_right, - x_lower_left, - y_lower_left, + upper_right_x, + upper_right_y, + lower_left_x, + lower_left_y, }) } fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), String> { - let min_x = &self.x_upper_right.min(self.x_lower_left); - let min_y = &self.y_upper_right.min(self.y_lower_left); - let max_x = &self.x_upper_right.max(self.x_lower_left); - let max_y = &self.y_upper_right.max(self.y_lower_left); + let min_x = &self.upper_right_x.min(self.lower_left_x); + let min_y = &self.upper_right_y.min(self.lower_left_y); + let max_x = &self.upper_right_x.max(self.lower_left_x); + let max_y = &self.upper_right_y.max(self.lower_left_y); buff.extend_from_slice(&max_x.to_be_bytes()); buff.extend_from_slice(&max_y.to_be_bytes()); @@ -159,10 +159,10 @@ mod box_tests { assert_eq!( pg_box, PgBox { - x_upper_right: 2., - y_upper_right: 2., - x_lower_left: -2., - y_lower_left: -2. + upper_right_x: 2., + upper_right_y: 2., + lower_left_x: -2., + lower_left_y: -2. } ) } @@ -173,10 +173,10 @@ mod box_tests { assert_eq!( pg_box, PgBox { - x_upper_right: 1., - y_upper_right: 2., - x_lower_left: 3., - y_lower_left: 4. + upper_right_x: 1., + upper_right_y: 2., + lower_left_x: 3., + lower_left_y: 4. } ); } @@ -186,10 +186,10 @@ mod box_tests { assert_eq!( pg_box, PgBox { - x_upper_right: 1., - y_upper_right: 2., - x_lower_left: 3., - y_lower_left: 4. + upper_right_x: 1., + upper_right_y: 2., + lower_left_x: 3., + lower_left_y: 4. } ); } @@ -200,10 +200,10 @@ mod box_tests { assert_eq!( pg_box, PgBox { - x_upper_right: 1., - y_upper_right: 2., - x_lower_left: 3., - y_lower_left: 4. + upper_right_x: 1., + upper_right_y: 2., + lower_left_x: 3., + lower_left_y: 4. } ); } @@ -214,10 +214,10 @@ mod box_tests { assert_eq!( pg_box, PgBox { - x_upper_right: 1., - y_upper_right: 2., - x_lower_left: 3., - y_lower_left: 4. + upper_right_x: 1., + upper_right_y: 2., + lower_left_x: 3., + lower_left_y: 4. } ); } @@ -243,7 +243,7 @@ mod box_tests { if let Err(err) = pg_box { assert_eq!( err.to_string(), - format!("error decoding BOX: could not get y_lower_left from {input_str}") + format!("error decoding BOX: could not get lower_left_y from {input_str}") ) } } @@ -256,7 +256,7 @@ mod box_tests { if let Err(err) = pg_box { assert_eq!( err.to_string(), - format!("error decoding BOX: could not get y_lower_left from {input_str}") + format!("error decoding BOX: could not get lower_left_y from {input_str}") ) } } @@ -267,10 +267,10 @@ mod box_tests { assert_eq!( pg_box, PgBox { - x_upper_right: 1.1, - y_upper_right: 2.2, - x_lower_left: 3.3, - y_lower_left: 4.4 + upper_right_x: 1.1, + upper_right_y: 2.2, + lower_left_x: 3.3, + lower_left_y: 4.4 } ); } @@ -278,10 +278,10 @@ mod box_tests { #[test] fn can_serialise_box_type_in_order() { let pg_box = PgBox { - x_upper_right: 2., - x_lower_left: -2., - y_upper_right: -2., - y_lower_left: 2., + upper_right_x: 2., + lower_left_x: -2., + upper_right_y: -2., + lower_left_y: 2., }; assert_eq!(pg_box.serialize_to_vec(), BOX_BYTES,) } @@ -289,10 +289,10 @@ mod box_tests { #[test] fn can_serialise_box_type_out_of_order() { let pg_box = PgBox { - x_upper_right: -2., - x_lower_left: 2., - y_upper_right: 2., - y_lower_left: -2., + upper_right_x: -2., + lower_left_x: 2., + upper_right_y: 2., + lower_left_y: -2., }; assert_eq!(pg_box.serialize_to_vec(), BOX_BYTES,) } @@ -300,10 +300,10 @@ mod box_tests { #[test] fn can_order_box() { let pg_box = PgBox { - x_upper_right: -2., - x_lower_left: 2., - y_upper_right: 2., - y_lower_left: -2., + upper_right_x: -2., + lower_left_x: 2., + upper_right_y: 2., + lower_left_y: -2., }; let bytes = pg_box.serialize_to_vec(); @@ -311,10 +311,10 @@ mod box_tests { assert_eq!( pg_box, PgBox { - x_upper_right: 2., - y_upper_right: 2., - x_lower_left: -2., - y_lower_left: -2. + upper_right_x: 2., + upper_right_y: 2., + lower_left_x: -2., + lower_left_y: -2. } ) } diff --git a/sqlx-postgres/src/types/geometry/line_segment.rs b/sqlx-postgres/src/types/geometry/line_segment.rs index a168ae25a2..ebe32d97d0 100644 --- a/sqlx-postgres/src/types/geometry/line_segment.rs +++ b/sqlx-postgres/src/types/geometry/line_segment.rs @@ -11,25 +11,25 @@ const ERROR: &str = "error decoding LSEG"; /// ## Postgres Geometric Line Segment type /// /// Description: Finite line segment -/// Representation: `((x1,y1),(x2,y2))` +/// Representation: `((start_x,start_y),(end_x,end_y))` /// /// /// Line segments are represented by pairs of points that are the endpoints of the segment. Values of type lseg are specified using any of the following syntaxes: /// ```text -/// [ ( x1 , y1 ) , ( x2 , y2 ) ] -/// ( ( x1 , y1 ) , ( x2 , y2 ) ) -/// ( x1 , y1 ) , ( x2 , y2 ) -/// x1 , y1 , x2 , y2 +/// [ ( start_x , start_y ) , ( end_x , end_y ) ] +/// ( ( start_x , start_y ) , ( end_x , end_y ) ) +/// ( start_x , start_y ) , ( end_x , end_y ) +/// start_x , start_y , end_x , end_y /// ``` -/// where `(x1,y1) and (x2,y2)` are the end points of the line segment. +/// where `(start_x,start_y) and (end_x,end_y)` are the end points of the line segment. /// /// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-LSEG #[derive(Debug, Clone, PartialEq)] pub struct PgLSeg { - pub x1: f64, - pub y1: f64, - pub x2: f64, - pub y2: f64, + pub start_x: f64, + pub start_y: f64, + pub end_x: f64, + pub end_y: f64, } impl Type for PgLSeg { @@ -71,49 +71,59 @@ impl FromStr for PgLSeg { let sanitised = s.replace(['(', ')', '[', ']', ' '], ""); let mut parts = sanitised.split(','); - let x1 = parts + let start_x = parts .next() .and_then(|s| s.parse::().ok()) - .ok_or_else(|| format!("{}: could not get x1 from {}", ERROR, s))?; + .ok_or_else(|| format!("{}: could not get start_x from {}", ERROR, s))?; - let y1 = parts + let start_y = parts .next() .and_then(|s| s.parse::().ok()) - .ok_or_else(|| format!("{}: could not get y1 from {}", ERROR, s))?; + .ok_or_else(|| format!("{}: could not get start_y from {}", ERROR, s))?; - let x2 = parts + let end_x = parts .next() .and_then(|s| s.parse::().ok()) - .ok_or_else(|| format!("{}: could not get x2 from {}", ERROR, s))?; + .ok_or_else(|| format!("{}: could not get end_x from {}", ERROR, s))?; - let y2 = parts + let end_y = parts .next() .and_then(|s| s.parse::().ok()) - .ok_or_else(|| format!("{}: could not get y2 from {}", ERROR, s))?; + .ok_or_else(|| format!("{}: could not get end_y from {}", ERROR, s))?; if parts.next().is_some() { return Err(format!("{}: too many numbers inputted in {}", ERROR, s).into()); } - Ok(PgLSeg { x1, y1, x2, y2 }) + Ok(PgLSeg { + start_x, + start_y, + end_x, + end_y, + }) } } impl PgLSeg { fn from_bytes(mut bytes: &[u8]) -> Result { - let x1 = bytes.get_f64(); - let y1 = bytes.get_f64(); - let x2 = bytes.get_f64(); - let y2 = bytes.get_f64(); - - Ok(PgLSeg { x1, y1, x2, y2 }) + let start_x = bytes.get_f64(); + let start_y = bytes.get_f64(); + let end_x = bytes.get_f64(); + let end_y = bytes.get_f64(); + + Ok(PgLSeg { + start_x, + start_y, + end_x, + end_y, + }) } fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), BoxDynError> { - buff.extend_from_slice(&self.x1.to_be_bytes()); - buff.extend_from_slice(&self.y1.to_be_bytes()); - buff.extend_from_slice(&self.x2.to_be_bytes()); - buff.extend_from_slice(&self.y2.to_be_bytes()); + buff.extend_from_slice(&self.start_x.to_be_bytes()); + buff.extend_from_slice(&self.start_y.to_be_bytes()); + buff.extend_from_slice(&self.end_x.to_be_bytes()); + buff.extend_from_slice(&self.end_y.to_be_bytes()); Ok(()) } @@ -143,10 +153,10 @@ mod lseg_tests { assert_eq!( lseg, PgLSeg { - x1: 1.1, - y1: 2.2, - x2: 3.3, - y2: 4.4 + start_x: 1.1, + start_y: 2.2, + end_x: 3.3, + end_y: 4.4 } ) } @@ -157,10 +167,10 @@ mod lseg_tests { assert_eq!( lseg, PgLSeg { - x1: 1., - y1: 2., - x2: 3., - y2: 4. + start_x: 1., + start_y: 2., + end_x: 3., + end_y: 4. } ); } @@ -170,10 +180,10 @@ mod lseg_tests { assert_eq!( lseg, PgLSeg { - x1: 1., - y1: 2., - x2: 3., - y2: 4. + start_x: 1., + start_y: 2., + end_x: 3., + end_y: 4. } ); } @@ -184,10 +194,10 @@ mod lseg_tests { assert_eq!( lseg, PgLSeg { - x1: 1., - y1: 2., - x2: 3., - y2: 4. + start_x: 1., + start_y: 2., + end_x: 3., + end_y: 4. } ); } @@ -198,10 +208,10 @@ mod lseg_tests { assert_eq!( lseg, PgLSeg { - x1: 1., - y1: 2., - x2: 3., - y2: 4. + start_x: 1., + start_y: 2., + end_x: 3., + end_y: 4. } ); } @@ -227,7 +237,7 @@ mod lseg_tests { if let Err(err) = lseg { assert_eq!( err.to_string(), - format!("error decoding LSEG: could not get y2 from {input_str}") + format!("error decoding LSEG: could not get end_y from {input_str}") ) } } @@ -240,7 +250,7 @@ mod lseg_tests { if let Err(err) = lseg { assert_eq!( err.to_string(), - format!("error decoding LSEG: could not get y2 from {input_str}") + format!("error decoding LSEG: could not get end_y from {input_str}") ) } } @@ -251,10 +261,10 @@ mod lseg_tests { assert_eq!( lseg, PgLSeg { - x1: 1.1, - y1: 2.2, - x2: 3.3, - y2: 4.4 + start_x: 1.1, + start_y: 2.2, + end_x: 3.3, + end_y: 4.4 } ); } @@ -262,10 +272,10 @@ mod lseg_tests { #[test] fn can_serialise_lseg_type() { let lseg = PgLSeg { - x1: 1.1, - y1: 2.2, - x2: 3.3, - y2: 4.4, + start_x: 1.1, + start_y: 2.2, + end_x: 3.3, + end_y: 4.4, }; assert_eq!(lseg.serialize_to_vec(), LINE_SEGMENT_BYTES,) } diff --git a/tests/postgres/types.rs b/tests/postgres/types.rs index 2aa27bea9d..acf139f10b 100644 --- a/tests/postgres/types.rs +++ b/tests/postgres/types.rs @@ -516,22 +516,22 @@ test_type!(_line>(Postgres, #[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] test_type!(lseg(Postgres, - "lseg('((1.0, 2.0), (3.0,4.0))')" @= sqlx::postgres::types::PgLSeg { x1: 1., y1: 2., x2: 3. , y2: 4.}, + "lseg('((1.0, 2.0), (3.0,4.0))')" @= sqlx::postgres::types::PgLSeg { start_x: 1., start_y: 2., end_x: 3. , end_y: 4.}, )); #[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] test_type!(_lseg>(Postgres, - "array[lseg('(1,2,3,4)'),lseg('[(1.1, 2.2), (3.3, 4.4)]')]" @= vec![sqlx::postgres::types::PgLSeg { x1: 1., y1: 2., x2: 3., y2: 4. }, sqlx::postgres::types::PgLSeg { x1: 1.1, y1: 2.2, x2: 3.3, y2: 4.4 }], + "array[lseg('(1,2,3,4)'),lseg('[(1.1, 2.2), (3.3, 4.4)]')]" @= vec![sqlx::postgres::types::PgLSeg { start_x: 1., start_y: 2., end_x: 3., end_y: 4. }, sqlx::postgres::types::PgLSeg { start_x: 1.1, start_y: 2.2, end_x: 3.3, end_y: 4.4 }], )); #[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] test_type!(box(Postgres, - "box('((1.0, 2.0), (3.0,4.0))')" @= sqlx::postgres::types::PgBox { x1: 3., y1: 4., x2: 1. , y2: 2.}, + "box('((1.0, 2.0), (3.0,4.0))')" @= sqlx::postgres::types::PgBox { upper_right_x: 3., upper_right_y: 4., lower_left_x: 1. , lower_left_y: 2.}, )); #[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] test_type!(_box>(Postgres, - "array[box('1,2,3,4'),box('((1.1, 2.2), (3.3, 4.4))')]" @= vec![sqlx::postgres::types::PgBox { x1: 3., y1: 4., x2: 1., y2: 2. }, sqlx::postgres::types::PgBox { x1: 3.3, y1: 4.4, x2: 1.1, y2: 2.2 }], + "array[box('1,2,3,4'),box('((1.1, 2.2), (3.3, 4.4))')]" @= vec![sqlx::postgres::types::PgBox { upper_right_x: 3., upper_right_y: 4., lower_left_x: 1., lower_left_y: 2. }, sqlx::postgres::types::PgBox { upper_right_x: 3.3, y1: 4.4, lower_left_x: 1.1, lower_left_y: 2.2 }], )); #[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] From 5e36b65e351da5e3ec3e33b84004ee25e4ad12ad Mon Sep 17 00:00:00 2001 From: James Holman Date: Sun, 8 Sep 2024 14:17:43 +1000 Subject: [PATCH 63/64] test: circle test radius --- tests/postgres/types.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/postgres/types.rs b/tests/postgres/types.rs index acf139f10b..6876d65b12 100644 --- a/tests/postgres/types.rs +++ b/tests/postgres/types.rs @@ -556,15 +556,15 @@ test_type!(polygon(Postgres, #[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] test_type!(circle(Postgres, - "circle('<(1.1, -2.2), 3.3>')" @= sqlx::postgres::types::PgCircle { x: 1.1, y:-2.2, r: 3.3 }, - "circle('((1.1, -2.2), 3.3)')" @= sqlx::postgres::types::PgCircle { x: 1.1, y:-2.2, r: 3.3 }, - "circle('(1.1, -2.2), 3.3')" @= sqlx::postgres::types::PgCircle { x: 1.1, y:-2.2, r: 3.3 }, - "circle('1.1, -2.2, 3.3')" @= sqlx::postgres::types::PgCircle { x: 1.1, y:-2.2, r: 3.3 }, + "circle('<(1.1, -2.2), 3.3>')" @= sqlx::postgres::types::PgCircle { x: 1.1, y:-2.2, radius: 3.3 }, + "circle('((1.1, -2.2), 3.3)')" @= sqlx::postgres::types::PgCircle { x: 1.1, y:-2.2, radius: 3.3 }, + "circle('(1.1, -2.2), 3.3')" @= sqlx::postgres::types::PgCircle { x: 1.1, y:-2.2, radius: 3.3 }, + "circle('1.1, -2.2, 3.3')" @= sqlx::postgres::types::PgCircle { x: 1.1, y:-2.2, radius: 3.3 }, )); #[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] test_type!(_circle>(Postgres, - "array[circle('<(1,2),3>'),circle('(1.1, 2.2), 3.3')]" @= vec![sqlx::postgres::types::PgCircle { x: 1., y: 2., r: 3. }, sqlx::postgres::types::PgCircle { x: 1.1, y: 2.2, r: 3.3 }], + "array[circle('<(1,2),3>'),circle('(1.1, 2.2), 3.3')]" @= vec![sqlx::postgres::types::PgCircle { x: 1., y: 2., radius: 3. }, sqlx::postgres::types::PgCircle { x: 1.1, y: 2.2, radius: 3.3 }], )); #[cfg(feature = "rust_decimal")] From 963d1d4bd703fa66dc3674283d2c9fec399bde6e Mon Sep 17 00:00:00 2001 From: James Holman Date: Sun, 8 Sep 2024 14:23:34 +1000 Subject: [PATCH 64/64] test: fix upper right y --- 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 6876d65b12..fa1ab08d5e 100644 --- a/tests/postgres/types.rs +++ b/tests/postgres/types.rs @@ -531,7 +531,7 @@ test_type!(box(Postgres, #[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] test_type!(_box>(Postgres, - "array[box('1,2,3,4'),box('((1.1, 2.2), (3.3, 4.4))')]" @= vec![sqlx::postgres::types::PgBox { upper_right_x: 3., upper_right_y: 4., lower_left_x: 1., lower_left_y: 2. }, sqlx::postgres::types::PgBox { upper_right_x: 3.3, y1: 4.4, lower_left_x: 1.1, lower_left_y: 2.2 }], + "array[box('1,2,3,4'),box('((1.1, 2.2), (3.3, 4.4))')]" @= vec![sqlx::postgres::types::PgBox { upper_right_x: 3., upper_right_y: 4., lower_left_x: 1., lower_left_y: 2. }, sqlx::postgres::types::PgBox { upper_right_x: 3.3, upper_right_y: 4.4, lower_left_x: 1.1, lower_left_y: 2.2 }], )); #[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))]