Skip to content

Commit 2f10c29

Browse files
authored
feat(postgres): add geometry circle (#3773)
* feat: circle * docs: comments
1 parent ca3a509 commit 2f10c29

File tree

11 files changed

+287
-6
lines changed

11 files changed

+287
-6
lines changed

sqlx-postgres/src/type_checking.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ impl_type_checking!(
4444

4545
sqlx::postgres::types::PgPolygon,
4646

47+
sqlx::postgres::types::PgCircle,
48+
4749
#[cfg(feature = "uuid")]
4850
sqlx::types::Uuid,
4951

sqlx-postgres/src/types/geometry/box.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ const ERROR: &str = "error decoding BOX";
2323
/// where `(upper_right_x,upper_right_y) and (lower_left_x,lower_left_y)` are any two opposite corners of the box.
2424
/// 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.
2525
///
26-
/// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-GEOMETRIC-BOXES
26+
/// See [Postgres Manual, Section 8.8.4: Geometric Types - Boxes][PG.S.8.8.4] for details.
27+
///
28+
/// [PG.S.8.8.4]: https://www.postgresql.org/docs/current/datatype-geometric.html#DATATYPE-GEOMETRIC-BOXES
29+
///
2730
#[derive(Debug, Clone, PartialEq)]
2831
pub struct PgBox {
2932
pub upper_right_x: f64,
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
use crate::decode::Decode;
2+
use crate::encode::{Encode, IsNull};
3+
use crate::error::BoxDynError;
4+
use crate::types::Type;
5+
use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, Postgres};
6+
use sqlx_core::bytes::Buf;
7+
use sqlx_core::Error;
8+
use std::str::FromStr;
9+
10+
const ERROR: &str = "error decoding CIRCLE";
11+
12+
/// ## Postgres Geometric Circle type
13+
///
14+
/// Description: Circle
15+
/// Representation: `< (x, y), radius >` (center point and radius)
16+
///
17+
/// ```text
18+
/// < ( x , y ) , radius >
19+
/// ( ( x , y ) , radius )
20+
/// ( x , y ) , radius
21+
/// x , y , radius
22+
/// ```
23+
/// where `(x,y)` is the center point.
24+
///
25+
/// See [Postgres Manual, Section 8.8.7, Geometric Types - Circles][PG.S.8.8.7] for details.
26+
///
27+
/// [PG.S.8.8.7]: https://www.postgresql.org/docs/current/datatype-geometric.html#DATATYPE-CIRCLE
28+
///
29+
#[derive(Debug, Clone, PartialEq)]
30+
pub struct PgCircle {
31+
pub x: f64,
32+
pub y: f64,
33+
pub radius: f64,
34+
}
35+
36+
impl Type<Postgres> for PgCircle {
37+
fn type_info() -> PgTypeInfo {
38+
PgTypeInfo::with_name("circle")
39+
}
40+
}
41+
42+
impl PgHasArrayType for PgCircle {
43+
fn array_type_info() -> PgTypeInfo {
44+
PgTypeInfo::with_name("_circle")
45+
}
46+
}
47+
48+
impl<'r> Decode<'r, Postgres> for PgCircle {
49+
fn decode(value: PgValueRef<'r>) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
50+
match value.format() {
51+
PgValueFormat::Text => Ok(PgCircle::from_str(value.as_str()?)?),
52+
PgValueFormat::Binary => Ok(PgCircle::from_bytes(value.as_bytes()?)?),
53+
}
54+
}
55+
}
56+
57+
impl<'q> Encode<'q, Postgres> for PgCircle {
58+
fn produces(&self) -> Option<PgTypeInfo> {
59+
Some(PgTypeInfo::with_name("circle"))
60+
}
61+
62+
fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
63+
self.serialize(buf)?;
64+
Ok(IsNull::No)
65+
}
66+
}
67+
68+
impl FromStr for PgCircle {
69+
type Err = BoxDynError;
70+
71+
fn from_str(s: &str) -> Result<Self, Self::Err> {
72+
let sanitised = s.replace(['<', '>', '(', ')', ' '], "");
73+
let mut parts = sanitised.split(',');
74+
75+
let x = parts
76+
.next()
77+
.and_then(|s| s.trim().parse::<f64>().ok())
78+
.ok_or_else(|| format!("{}: could not get x from {}", ERROR, s))?;
79+
80+
let y = parts
81+
.next()
82+
.and_then(|s| s.trim().parse::<f64>().ok())
83+
.ok_or_else(|| format!("{}: could not get y from {}", ERROR, s))?;
84+
85+
let radius = parts
86+
.next()
87+
.and_then(|s| s.trim().parse::<f64>().ok())
88+
.ok_or_else(|| format!("{}: could not get radius from {}", ERROR, s))?;
89+
90+
if parts.next().is_some() {
91+
return Err(format!("{}: too many numbers inputted in {}", ERROR, s).into());
92+
}
93+
94+
if radius < 0. {
95+
return Err(format!("{}: cannot have negative radius: {}", ERROR, s).into());
96+
}
97+
98+
Ok(PgCircle { x, y, radius })
99+
}
100+
}
101+
102+
impl PgCircle {
103+
fn from_bytes(mut bytes: &[u8]) -> Result<PgCircle, Error> {
104+
let x = bytes.get_f64();
105+
let y = bytes.get_f64();
106+
let r = bytes.get_f64();
107+
Ok(PgCircle { x, y, radius: r })
108+
}
109+
110+
fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), Error> {
111+
buff.extend_from_slice(&self.x.to_be_bytes());
112+
buff.extend_from_slice(&self.y.to_be_bytes());
113+
buff.extend_from_slice(&self.radius.to_be_bytes());
114+
Ok(())
115+
}
116+
117+
#[cfg(test)]
118+
fn serialize_to_vec(&self) -> Vec<u8> {
119+
let mut buff = PgArgumentBuffer::default();
120+
self.serialize(&mut buff).unwrap();
121+
buff.to_vec()
122+
}
123+
}
124+
125+
#[cfg(test)]
126+
mod circle_tests {
127+
128+
use std::str::FromStr;
129+
130+
use super::PgCircle;
131+
132+
const CIRCLE_BYTES: &[u8] = &[
133+
63, 241, 153, 153, 153, 153, 153, 154, 64, 1, 153, 153, 153, 153, 153, 154, 64, 10, 102,
134+
102, 102, 102, 102, 102,
135+
];
136+
137+
#[test]
138+
fn can_deserialise_circle_type_bytes() {
139+
let circle = PgCircle::from_bytes(CIRCLE_BYTES).unwrap();
140+
assert_eq!(
141+
circle,
142+
PgCircle {
143+
x: 1.1,
144+
y: 2.2,
145+
radius: 3.3
146+
}
147+
)
148+
}
149+
150+
#[test]
151+
fn can_deserialise_circle_type_str() {
152+
let circle = PgCircle::from_str("<(1, 2), 3 >").unwrap();
153+
assert_eq!(
154+
circle,
155+
PgCircle {
156+
x: 1.0,
157+
y: 2.0,
158+
radius: 3.0
159+
}
160+
);
161+
}
162+
163+
#[test]
164+
fn can_deserialise_circle_type_str_second_syntax() {
165+
let circle = PgCircle::from_str("((1, 2), 3 )").unwrap();
166+
assert_eq!(
167+
circle,
168+
PgCircle {
169+
x: 1.0,
170+
y: 2.0,
171+
radius: 3.0
172+
}
173+
);
174+
}
175+
176+
#[test]
177+
fn can_deserialise_circle_type_str_third_syntax() {
178+
let circle = PgCircle::from_str("(1, 2), 3 ").unwrap();
179+
assert_eq!(
180+
circle,
181+
PgCircle {
182+
x: 1.0,
183+
y: 2.0,
184+
radius: 3.0
185+
}
186+
);
187+
}
188+
189+
#[test]
190+
fn can_deserialise_circle_type_str_fourth_syntax() {
191+
let circle = PgCircle::from_str("1, 2, 3 ").unwrap();
192+
assert_eq!(
193+
circle,
194+
PgCircle {
195+
x: 1.0,
196+
y: 2.0,
197+
radius: 3.0
198+
}
199+
);
200+
}
201+
202+
#[test]
203+
fn cannot_deserialise_circle_invalid_numbers() {
204+
let input_str = "1, 2, Three";
205+
let circle = PgCircle::from_str(input_str);
206+
assert!(circle.is_err());
207+
if let Err(err) = circle {
208+
assert_eq!(
209+
err.to_string(),
210+
format!("error decoding CIRCLE: could not get radius from {input_str}")
211+
)
212+
}
213+
}
214+
215+
#[test]
216+
fn cannot_deserialise_circle_negative_radius() {
217+
let input_str = "1, 2, -3";
218+
let circle = PgCircle::from_str(input_str);
219+
assert!(circle.is_err());
220+
if let Err(err) = circle {
221+
assert_eq!(
222+
err.to_string(),
223+
format!("error decoding CIRCLE: cannot have negative radius: {input_str}")
224+
)
225+
}
226+
}
227+
228+
#[test]
229+
fn can_deserialise_circle_type_str_float() {
230+
let circle = PgCircle::from_str("<(1.1, 2.2), 3.3>").unwrap();
231+
assert_eq!(
232+
circle,
233+
PgCircle {
234+
x: 1.1,
235+
y: 2.2,
236+
radius: 3.3
237+
}
238+
);
239+
}
240+
241+
#[test]
242+
fn can_serialise_circle_type() {
243+
let circle = PgCircle {
244+
x: 1.1,
245+
y: 2.2,
246+
radius: 3.3,
247+
};
248+
assert_eq!(circle.serialize_to_vec(), CIRCLE_BYTES,)
249+
}
250+
}

sqlx-postgres/src/types/geometry/line.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ const ERROR: &str = "error decoding LINE";
1515
///
1616
/// Lines are represented by the linear equation Ax + By + C = 0, where A and B are not both zero.
1717
///
18-
/// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-LINE
18+
/// See [Postgres Manual, Section 8.8.2, Geometric Types - Lines][PG.S.8.8.2] for details.
19+
///
20+
/// [PG.S.8.8.2]: https://www.postgresql.org/docs/current/datatype-geometric.html#DATATYPE-LINE
21+
///
1922
#[derive(Debug, Clone, PartialEq)]
2023
pub struct PgLine {
2124
pub a: f64,

sqlx-postgres/src/types/geometry/line_segment.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ const ERROR: &str = "error decoding LSEG";
2323
/// ```
2424
/// where `(start_x,start_y) and (end_x,end_y)` are the end points of the line segment.
2525
///
26-
/// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-LSEG
26+
/// See [Postgres Manual, Section 8.8.3, Geometric Types - Line Segments][PG.S.8.8.3] for details.
27+
///
28+
/// [PG.S.8.8.3]: https://www.postgresql.org/docs/current/datatype-geometric.html#DATATYPE-LSEG
29+
///
2730
#[doc(alias = "line segment")]
2831
#[derive(Debug, Clone, PartialEq)]
2932
pub struct PgLSeg {

sqlx-postgres/src/types/geometry/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub mod r#box;
2+
pub mod circle;
23
pub mod line;
34
pub mod line_segment;
45
pub mod path;

sqlx-postgres/src/types/geometry/path.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ const BYTE_WIDTH: usize = mem::size_of::<f64>();
2727
/// 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.
2828
/// When the outermost parentheses are omitted, as in the third through fifth syntaxes, a closed path is assumed.
2929
///
30-
/// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-GEOMETRIC-PATHS
30+
/// See [Postgres Manual, Section 8.8.5, Geometric Types - Paths][PG.S.8.8.5] for details.
31+
///
32+
/// [PG.S.8.8.5]: https://www.postgresql.org/docs/current/datatype-geometric.html#DATATYPE-GEOMETRIC-PATHS
33+
///
3134
#[derive(Debug, Clone, PartialEq)]
3235
pub struct PgPath {
3336
pub closed: bool,

sqlx-postgres/src/types/geometry/point.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ use std::str::FromStr;
1919
/// ````
2020
/// where x and y are the respective coordinates, as floating-point numbers.
2121
///
22-
/// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-GEOMETRIC-POINTS
22+
/// See [Postgres Manual, Section 8.8.1, Geometric Types - Points][PG.S.8.8.1] for details.
23+
///
24+
/// [PG.S.8.8.1]: https://www.postgresql.org/docs/current/datatype-geometric.html#DATATYPE-GEOMETRIC-POINTS
25+
///
2326
#[derive(Debug, Clone, PartialEq)]
2427
pub struct PgPoint {
2528
pub x: f64,

sqlx-postgres/src/types/geometry/polygon.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ const BYTE_WIDTH: usize = mem::size_of::<f64>();
2828
///
2929
/// where the points are the end points of the line segments comprising the boundary of the polygon.
3030
///
31-
/// Seeh ttps://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-POLYGON
31+
/// See [Postgres Manual, Section 8.8.6, Geometric Types - Polygons][PG.S.8.8.6] for details.
32+
///
33+
/// [PG.S.8.8.6]: https://www.postgresql.org/docs/current/datatype-geometric.html#DATATYPE-POLYGON
34+
///
3235
#[derive(Debug, Clone, PartialEq)]
3336
pub struct PgPolygon {
3437
pub points: Vec<PgPoint>,

sqlx-postgres/src/types/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
//! | [`PgBox`] | BOX |
2828
//! | [`PgPath`] | PATH |
2929
//! | [`PgPolygon`] | POLYGON |
30+
//! | [`PgCircle`] | CIRCLE |
3031
//! | [`PgHstore`] | HSTORE |
3132
//!
3233
//! <sup>1</sup> SQLx generally considers `CITEXT` to be compatible with `String`, `&str`, etc.,
@@ -262,6 +263,7 @@ mod bit_vec;
262263
pub use array::PgHasArrayType;
263264
pub use citext::PgCiText;
264265
pub use cube::PgCube;
266+
pub use geometry::circle::PgCircle;
265267
pub use geometry::line::PgLine;
266268
pub use geometry::line_segment::PgLSeg;
267269
pub use geometry::path::PgPath;

tests/postgres/types.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,14 @@ test_type!(polygon<sqlx::postgres::types::PgPolygon>(Postgres,
539539
]},
540540
));
541541

542+
#[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))]
543+
test_type!(circle<sqlx::postgres::types::PgCircle>(Postgres,
544+
"circle('<(1.1, -2.2), 3.3>')" ~= sqlx::postgres::types::PgCircle { x: 1.1, y:-2.2, radius: 3.3 },
545+
"circle('((1.1, -2.2), 3.3)')" ~= sqlx::postgres::types::PgCircle { x: 1.1, y:-2.2, radius: 3.3 },
546+
"circle('(1.1, -2.2), 3.3')" ~= sqlx::postgres::types::PgCircle { x: 1.1, y:-2.2, radius: 3.3 },
547+
"circle('1.1, -2.2, 3.3')" ~= sqlx::postgres::types::PgCircle { x: 1.1, y:-2.2, radius: 3.3 },
548+
));
549+
542550
#[cfg(feature = "rust_decimal")]
543551
test_type!(decimal<sqlx::types::Decimal>(Postgres,
544552
"0::numeric" == sqlx::types::Decimal::from_str("0").unwrap(),

0 commit comments

Comments
 (0)