Skip to main content

elefant_client/types/
point_type.rs

1use crate::protocol::FieldDescription;
2use crate::types::{FromSqlBase, FromSqlBinary, FromSqlText, PostgresType, ToSql};
3use std::error::Error;
4
5/// PostgreSQL POINT geometric type representing x,y coordinates
6#[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        // PostgreSQL POINT binary format: two consecutive float8 (f64) values
30        if raw.len() != 16 {
31            return Err(format!("Expected 16 bytes for POINT, got {}", raw.len()).into());
32        }
33
34        // Extract two big-endian f64 values (x, y coordinates)
35        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        // PostgreSQL POINT text format: (x,y)
55        // In arrays, points may be quoted: "(x,y)"
56        let trimmed = raw.trim();
57
58        // Handle quoted points in arrays by stripping outer quotes
59        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        // Based on reverse engineering, assume two big-endian f64 values
101        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                // Use approximate equality for floating point comparison
187                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                // Clean up for next iteration
201                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}