elefant_client/types/
point_type.rs1use crate::protocol::FieldDescription;
2use crate::types::{FromSqlBase, FromSqlBinary, FromSqlText, PostgresType, ToSql};
3use std::error::Error;
4
5#[derive(Debug, Clone, Copy, PartialEq)]
7pub struct Point {
8 pub x: f64,
9 pub y: f64,
10}
11
12impl Point {
13 pub fn new(x: f64, y: f64) -> Self {
14 Point { x, y }
15 }
16}
17
18impl<'a> FromSqlBase<'a> for Point {
19 fn accepts_postgres_type(oid: i32) -> bool {
20 oid == PostgresType::POINT.oid
21 }
22}
23
24impl<'a> FromSqlBinary<'a> for Point {
25 fn from_sql_binary(
26 raw: &'a [u8],
27 _field: &FieldDescription,
28 ) -> Result<Self, Box<dyn Error + Sync + Send>> {
29 if raw.len() != 16 {
31 return Err(format!("Expected 16 bytes for POINT, got {}", raw.len()).into());
32 }
33
34 let x_bytes: [u8; 8] = raw[0..8]
36 .try_into()
37 .map_err(|e| format!("Failed to extract x coordinate bytes: {e}"))?;
38 let y_bytes: [u8; 8] = raw[8..16]
39 .try_into()
40 .map_err(|e| format!("Failed to extract y coordinate bytes: {e}"))?;
41
42 let x = f64::from_be_bytes(x_bytes);
43 let y = f64::from_be_bytes(y_bytes);
44
45 Ok(Point { x, y })
46 }
47}
48
49impl<'a> FromSqlText<'a> for Point {
50 fn from_sql_text(
51 raw: &'a str,
52 _field: &FieldDescription,
53 ) -> Result<Self, Box<dyn Error + Sync + Send>> {
54 let trimmed = raw.trim();
57
58 let unquoted = if trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() >= 2 {
60 &trimmed[1..trimmed.len() - 1]
61 } else {
62 trimmed
63 };
64
65 if !unquoted.starts_with('(') || !unquoted.ends_with(')') {
66 return Err(
67 format!("Invalid POINT text format: '{raw}' - expected format: (x,y)").into(),
68 );
69 }
70
71 let inner = &unquoted[1..unquoted.len() - 1];
72 let parts: Vec<&str> = inner.split(',').collect();
73 if parts.len() != 2 {
74 return Err(format!(
75 "POINT must have exactly 2 coordinates, got {} in '{}'",
76 parts.len(),
77 raw
78 )
79 .into());
80 }
81
82 let x: f64 = parts[0]
83 .trim()
84 .parse()
85 .map_err(|e| format!("Failed to parse x coordinate '{}': {}", parts[0].trim(), e))?;
86 let y: f64 = parts[1]
87 .trim()
88 .parse()
89 .map_err(|e| format!("Failed to parse y coordinate '{}': {}", parts[1].trim(), e))?;
90
91 Ok(Point { x, y })
92 }
93}
94
95impl ToSql for Point {
96 fn to_sql_binary(
97 &self,
98 target_buffer: &mut Vec<u8>,
99 ) -> Result<(), Box<dyn Error + Sync + Send>> {
100 target_buffer.extend_from_slice(&self.x.to_be_bytes());
102 target_buffer.extend_from_slice(&self.y.to_be_bytes());
103 Ok(())
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 #[cfg(feature = "tokio")]
110 mod tokio_connection {
111 use crate::test_helpers::get_settings;
112 use crate::tokio_connection::new_client;
113 use crate::Point;
114 use tokio::test;
115
116 #[test]
117 async fn test_point_edge_cases() {
118 let mut client = new_client(get_settings()).await.unwrap();
119
120 let large: Point = client
121 .read_single_value_dual_mode::<Point>("select '(1e10, -1e10)'::point")
122 .await;
123 assert_eq!(large.x, 1e10);
124 assert_eq!(large.y, -1e10);
125
126 let small: Point = client
127 .read_single_value_dual_mode::<Point>("select '(1e-10, -1e-10)'::point")
128 .await;
129 assert_eq!(small.x, 1e-10);
130 assert_eq!(small.y, -1e-10);
131
132 let infinity: Point = client
133 .read_single_value_dual_mode::<Point>("select '(Infinity, -Infinity)'::point")
134 .await;
135 assert!(infinity.x.is_infinite() && infinity.x.is_sign_positive());
136 assert!(infinity.y.is_infinite() && infinity.y.is_sign_negative());
137 }
138
139 #[test]
140 async fn test_point_basic_values() {
141 let mut client = new_client(get_settings()).await.unwrap();
142
143 let origin: Point = client
144 .read_single_value_dual_mode::<Point>("select '(0, 0)'::point")
145 .await;
146 assert_eq!(origin.x, 0.0);
147 assert_eq!(origin.y, 0.0);
148
149 let positive: Point = client
150 .read_single_value_dual_mode::<Point>("select '(1.5, 2.5)'::point")
151 .await;
152 assert_eq!(positive.x, 1.5);
153 assert_eq!(positive.y, 2.5);
154
155 let negative: Point = client
156 .read_single_value_dual_mode::<Point>("select '(-3.17, -2.71)'::point")
157 .await;
158 assert_eq!(negative.x, -3.17);
159 assert_eq!(negative.y, -2.71);
160 }
161
162 #[test]
163 async fn test_point_round_trip() {
164 let mut client = new_client(get_settings()).await.unwrap();
165
166 client.execute_non_query_simple("drop table if exists test_point_table; create table test_point_table(location point);").await.unwrap();
167
168 let test_points = vec![
169 Point::new(0.0, 0.0),
170 Point::new(1.0, 1.0),
171 Point::new(-1.0, -1.0),
172 Point::new(123.456, 789.012),
173 Point::new(-999.999, 123.123),
174 ];
175
176 for test_point in &test_points {
177 client
178 .execute_non_query("insert into test_point_table values ($1);", &[test_point])
179 .await
180 .unwrap();
181
182 let retrieved: Point = client
183 .read_single_value("select location from test_point_table order by location <-> point(0,0) limit 1;", &[])
184 .await;
185
186 assert!(
188 (retrieved.x - test_point.x).abs() < f64::EPSILON,
189 "X coordinate mismatch: {} != {}",
190 retrieved.x,
191 test_point.x
192 );
193 assert!(
194 (retrieved.y - test_point.y).abs() < f64::EPSILON,
195 "Y coordinate mismatch: {} != {}",
196 retrieved.y,
197 test_point.y
198 );
199
200 client
202 .execute_non_query("delete from test_point_table;", &[])
203 .await
204 .unwrap();
205 }
206 }
207
208 #[test]
209 async fn test_point_null_handling() {
210 let mut client = new_client(get_settings()).await.unwrap();
211
212 let null_point: Option<Point> = client
213 .read_single_value_dual_mode("select null::point")
214 .await;
215 assert_eq!(null_point, None);
216 }
217
218 #[test]
219 async fn test_point_array_support() {
220 let mut client = new_client(get_settings()).await.unwrap();
221
222 let point_array: Vec<Point> = client
223 .read_single_value_dual_mode::<Vec<Point>>(
224 "select ARRAY[point(0,0), point(1,1), point(-1,-1)]",
225 )
226 .await;
227
228 let expected = [
229 Point::new(0.0, 0.0),
230 Point::new(1.0, 1.0),
231 Point::new(-1.0, -1.0),
232 ];
233
234 assert_eq!(point_array.len(), expected.len());
235 for (actual, expected) in point_array.iter().zip(expected.iter()) {
236 assert!((actual.x - expected.x).abs() < f64::EPSILON);
237 assert!((actual.y - expected.y).abs() < f64::EPSILON);
238 }
239 }
240 }
241}