Skip to main content

sentinel_driver/types/
cube.rs

1use bytes::{BufMut, BytesMut};
2
3use crate::error::{Error, Result};
4use crate::types::{FromSql, Oid, ToSql};
5
6/// PostgreSQL CUBE type -- an n-dimensional point or box.
7///
8/// CUBE is a PostgreSQL extension type for multi-dimensional geometric data.
9/// It represents either a point (all dimensions equal) or a box (two corners).
10///
11/// Binary wire format:
12/// - `ndim` (u32): number of dimensions
13/// - `flags` (u32): bit 0 = is_point
14/// - coordinates: `ndim` f64 values for points, `ndim * 2` for boxes
15///
16/// Uses TEXT OID as carrier since CUBE is an extension type.
17#[derive(Debug, Clone, PartialEq)]
18pub struct PgCube {
19    /// For a point: `[x, y, z, ...]` (ndim values).
20    /// For a box: `[x1, y1, z1, ..., x2, y2, z2, ...]` (ndim * 2 values).
21    pub coordinates: Vec<f64>,
22    /// True if this represents a point, false if a box.
23    pub is_point: bool,
24}
25
26const CUBE_IS_POINT: u32 = 1;
27
28impl PgCube {
29    /// Create a point with the given coordinates.
30    pub fn point(coordinates: Vec<f64>) -> Self {
31        PgCube {
32            coordinates,
33            is_point: true,
34        }
35    }
36
37    /// Create a box with the given coordinates and number of dimensions.
38    ///
39    /// `coordinates` must have exactly `ndim * 2` values:
40    /// the first `ndim` are the lower-left corner, the last `ndim` are the upper-right.
41    pub fn cube(coordinates: Vec<f64>, ndim: usize) -> Self {
42        debug_assert_eq!(
43            coordinates.len(),
44            ndim * 2,
45            "box coordinates must be ndim * 2"
46        );
47        PgCube {
48            coordinates,
49            is_point: false,
50        }
51    }
52
53    /// Number of dimensions.
54    pub fn ndim(&self) -> usize {
55        if self.is_point {
56            self.coordinates.len()
57        } else if self.coordinates.is_empty() {
58            0
59        } else {
60            self.coordinates.len() / 2
61        }
62    }
63}
64
65impl ToSql for PgCube {
66    fn oid(&self) -> Oid {
67        Oid::TEXT
68    }
69
70    fn to_sql(&self, buf: &mut BytesMut) -> Result<()> {
71        let ndim = self.ndim() as u32;
72        let flags = if self.is_point { CUBE_IS_POINT } else { 0 };
73
74        buf.put_u32(ndim);
75        buf.put_u32(flags);
76
77        for &coord in &self.coordinates {
78            buf.put_f64(coord);
79        }
80
81        Ok(())
82    }
83}
84
85impl FromSql for PgCube {
86    fn oid() -> Oid {
87        Oid::TEXT
88    }
89
90    fn from_sql(buf: &[u8]) -> Result<Self> {
91        if buf.len() < 8 {
92            return Err(Error::Decode(format!(
93                "cube: expected at least 8 bytes, got {}",
94                buf.len()
95            )));
96        }
97
98        let ndim = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]) as usize;
99        let flags = u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]);
100        let is_point = (flags & CUBE_IS_POINT) != 0;
101
102        let num_coords = if is_point { ndim } else { ndim * 2 };
103        let expected_len = 8 + num_coords * 8;
104
105        if buf.len() < expected_len {
106            return Err(Error::Decode(format!(
107                "cube: expected {} bytes for {ndim}D {}, got {}",
108                expected_len,
109                if is_point { "point" } else { "box" },
110                buf.len()
111            )));
112        }
113
114        let mut coordinates = Vec::with_capacity(num_coords);
115        let mut offset = 8;
116        for _ in 0..num_coords {
117            let val = f64::from_be_bytes([
118                buf[offset],
119                buf[offset + 1],
120                buf[offset + 2],
121                buf[offset + 3],
122                buf[offset + 4],
123                buf[offset + 5],
124                buf[offset + 6],
125                buf[offset + 7],
126            ]);
127            coordinates.push(val);
128            offset += 8;
129        }
130
131        Ok(PgCube {
132            coordinates,
133            is_point,
134        })
135    }
136}
137
138impl std::fmt::Display for PgCube {
139    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140        if self.is_point {
141            write!(f, "(")?;
142            for (i, coord) in self.coordinates.iter().enumerate() {
143                if i > 0 {
144                    write!(f, ", ")?;
145                }
146                write!(f, "{coord}")?;
147            }
148            write!(f, ")")
149        } else {
150            let ndim = self.ndim();
151            let (lower, upper) = self.coordinates.split_at(ndim);
152            write!(f, "(")?;
153            for (i, coord) in lower.iter().enumerate() {
154                if i > 0 {
155                    write!(f, ", ")?;
156                }
157                write!(f, "{coord}")?;
158            }
159            write!(f, "),(")?;
160            for (i, coord) in upper.iter().enumerate() {
161                if i > 0 {
162                    write!(f, ", ")?;
163                }
164                write!(f, "{coord}")?;
165            }
166            write!(f, ")")
167        }
168    }
169}