rbdc_pg/types/
point.rs

1use crate::types::decode::Decode;
2use crate::types::encode::{Encode, IsNull};
3use crate::value::{PgValue, PgValueFormat};
4use rbdc::Error;
5use rbs::Value;
6use std::fmt::{Display, Formatter};
7
8/// PostgreSQL POINT type for geometric points
9///
10/// Represents a point in 2D space (x, y).
11/// This implementation uses WKT (Well-Known Text) format for text representation.
12///
13/// # Examples
14///
15/// ```ignore
16/// // Create a point at (116.4, 39.9) - Beijing coordinates
17/// let point = Point { x: 116.4, y: 39.9 };
18///
19/// // WKT format: "POINT(116.4 39.9)"
20/// ```
21///
22/// For more advanced GIS operations, consider using PostGIS extension directly
23/// or the `geo-types` crate for parsing WKT/WKB formats.
24#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, PartialEq)]
25pub struct Point {
26    pub x: f64,
27    pub y: f64,
28}
29
30impl Display for Point {
31    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
32        write!(f, "POINT({} {})", self.x, self.y)
33    }
34}
35
36impl From<Point> for Value {
37    fn from(arg: Point) -> Self {
38        rbs::Value::Ext("point", Box::new(rbs::Value::Ext(
39            "point",
40            Box::new(rbs::Value::Array(vec![
41                rbs::Value::F64(arg.x),
42                rbs::Value::F64(arg.y),
43            ]))
44        )))
45    }
46}
47
48impl Decode for Point {
49    fn decode(value: PgValue) -> Result<Self, Error> {
50        Ok(match value.format() {
51            PgValueFormat::Binary => {
52                // Binary format is WKB (Well-Known Binary)
53                // For simplicity, we don't support direct binary parsing
54                // Applications should use geo-types crate for proper WKB parsing
55                // Or use TEXT format in PostgreSQL: ST_AsText(point_column)
56                return Err(Error::from(
57                    "POINT binary format (WKB) not supported. \
58                     Use TEXT format: ST_AsText(point_column) or parse with geo-types crate."
59                ));
60            }
61            PgValueFormat::Text => {
62                // Text format is WKT (Well-Known Text): "POINT(x y)"
63                let s = value.as_str()?;
64
65                // Parse WKT format: "POINT(x y)"
66                let s = s.trim();
67                if !s.starts_with("POINT(") || !s.ends_with(')') {
68                    return Err(Error::from(format!(
69                        "Invalid POINT format: {}. Expected 'POINT(x y)'",
70                        s
71                    )));
72                }
73
74                let coords = &s[6..s.len()-1]; // Remove "POINT(" and ")"
75                let parts: Vec<&str> = coords.split_whitespace().collect();
76
77                if parts.len() != 2 {
78                    return Err(Error::from(format!(
79                        "Invalid POINT coords: {}. Expected 2 values.",
80                        coords
81                    )));
82                }
83
84                let x = parts[0].parse::<f64>()
85                    .map_err(|e| Error::from(format!("Invalid x coordinate: {}", e)))?;
86                let y = parts[1].parse::<f64>()
87                    .map_err(|e| Error::from(format!("Invalid y coordinate: {}", e)))?;
88
89                Self { x, y }
90            }
91        })
92    }
93}
94
95impl Encode for Point {
96    fn encode(self, _buf: &mut crate::arguments::PgArgumentBuffer) -> Result<IsNull, Error> {
97        // For PostGIS POINT, use TEXT format in your query:
98        // ST_GeomFromText('POINT(116.4 39.9)')
99        Err(Error::from(
100            "POINT encoding not supported. Use PostGIS ST_GeomFromText() or ST_MakePoint() in your query instead."
101        ))
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use crate::types::decode::Decode;
109    use crate::value::{PgValue, PgValueFormat};
110
111    #[test]
112    fn test_display() {
113        let point = Point { x: 116.4, y: 39.9 };
114        assert_eq!(format!("{}", point), "POINT(116.4 39.9)");
115    }
116
117    #[test]
118    fn test_beijing_coords() {
119        let point = Point { x: 116.4074, y: 39.9042 };
120        let display = format!("{}", point);
121        println!("Beijing: {}", display);
122        assert!(display.contains("POINT("));
123    }
124
125    #[test]
126    fn test_decode_text_valid() {
127        let s = "POINT(116.4 39.9)";
128        let result: Point = Decode::decode(PgValue {
129            value: Some(s.as_bytes().to_vec()),
130            type_info: crate::type_info::PgTypeInfo::UNKNOWN,
131            format: PgValueFormat::Text,
132            timezone_sec: None,
133        }).unwrap();
134        assert_eq!(result.x, 116.4);
135        assert_eq!(result.y, 39.9);
136    }
137
138    #[test]
139    fn test_decode_text_negative_coords() {
140        let s = "POINT(-74.0060 40.7128)";
141        let result: Point = Decode::decode(PgValue {
142            value: Some(s.as_bytes().to_vec()),
143            type_info: crate::type_info::PgTypeInfo::UNKNOWN,
144            format: PgValueFormat::Text,
145            timezone_sec: None,
146        }).unwrap();
147        assert_eq!(result.x, -74.0060);
148        assert_eq!(result.y, 40.7128);
149    }
150
151    #[test]
152    fn test_decode_text_invalid_format() {
153        let s = "INVALID(116.4 39.9)";
154        let result: Result<Point, _> = Decode::decode(PgValue {
155            value: Some(s.as_bytes().to_vec()),
156            type_info: crate::type_info::PgTypeInfo::UNKNOWN,
157            format: PgValueFormat::Text,
158            timezone_sec: None,
159        });
160        assert!(result.is_err());
161    }
162
163    #[test]
164    fn test_decode_text_invalid_coords_count() {
165        let s = "POINT(116.4)";
166        let result: Result<Point, _> = Decode::decode(PgValue {
167            value: Some(s.as_bytes().to_vec()),
168            type_info: crate::type_info::PgTypeInfo::UNKNOWN,
169            format: PgValueFormat::Text,
170            timezone_sec: None,
171        });
172        assert!(result.is_err());
173    }
174
175    #[test]
176    fn test_from_value() {
177        let point = Point { x: 1.0, y: 2.0 };
178        let value: rbs::Value = point.into();
179        match value {
180            rbs::Value::Ext(type_name, boxed) => {
181                assert_eq!(type_name, "point");
182                if let rbs::Value::Ext(inner_type, inner_boxed) = *boxed {
183                    assert_eq!(inner_type, "point");
184                    if let rbs::Value::Array(arr) = *inner_boxed {
185                        assert_eq!(arr.len(), 2);
186                        if let rbs::Value::F64(x) = &arr[0] {
187                            assert_eq!(*x, 1.0);
188                        } else {
189                            panic!("Expected F64");
190                        }
191                        if let rbs::Value::F64(y) = &arr[1] {
192                            assert_eq!(*y, 2.0);
193                        } else {
194                            panic!("Expected F64");
195                        }
196                    } else {
197                        panic!("Expected Array");
198                    }
199                } else {
200                    panic!("Expected inner Ext");
201                }
202            }
203            _ => panic!("Expected Ext variant"),
204        }
205    }
206
207    #[test]
208    fn test_equality() {
209        let p1 = Point { x: 1.0, y: 2.0 };
210        let p2 = Point { x: 1.0, y: 2.0 };
211        let p3 = Point { x: 2.0, y: 3.0 };
212        assert_eq!(p1, p2);
213        assert_ne!(p1, p3);
214    }
215
216    #[test]
217    fn test_clone() {
218        let p1 = Point { x: 1.0, y: 2.0 };
219        let p2 = p1;
220        assert_eq!(p1, p2);
221    }
222}