rbdc_pg/types/
hstore.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::collections::HashMap;
7use std::fmt::{Display, Formatter};
8
9/// PostgreSQL HStore type for key-value pairs
10///
11/// HStore is a PostgreSQL extension module that implements the hstore data type
12/// for storing sets of key/value pairs within a single PostgreSQL value.
13///
14/// # Examples
15///
16/// ```ignore
17/// // Create an hstore from a HashMap
18/// let mut map = HashMap::new();
19/// map.insert("name".to_string(), "John".to_string());
20/// map.insert("age".to_string(), "30".to_string());
21/// let hstore = Hstore(map);
22///
23/// // Text format representation: "name=>John, age=>30"
24/// ```
25///
26/// This implementation supports both TEXT and BINARY formats:
27/// - TEXT: "key1=>value1, key2=>value2"
28/// - BINARY: 32-bit header + count + entries
29#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
30pub struct Hstore(pub HashMap<String, String>);
31
32impl Default for Hstore {
33    fn default() -> Self {
34        Self(HashMap::new())
35    }
36}
37
38impl Display for Hstore {
39    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
40        let pairs: Vec<String> = self.0
41            .iter()
42            .map(|(k, v)| format!("{}=>{}", k, v))
43            .collect();
44        write!(f, "{}", pairs.join(", "))
45    }
46}
47
48impl From<HashMap<String, String>> for Hstore {
49    fn from(map: HashMap<String, String>) -> Self {
50        Self(map)
51    }
52}
53
54impl From<Hstore> for Value {
55    fn from(arg: Hstore) -> Self {
56        // Store as string representation: "key1=>value1, key2=>value2"
57        let s = format!("{}", arg);
58        Value::Ext("hstore", Box::new(Value::String(s)))
59    }
60}
61
62impl Decode for Hstore {
63    fn decode(value: PgValue) -> Result<Self, Error> {
64        Ok(match value.format() {
65            PgValueFormat::Binary => {
66                // Binary format:
67                // 4 bytes: number of entries (int32)
68                // For each entry:
69                //   4 bytes: key length
70                //   key bytes
71                //   4 bytes: value length (-1 for NULL)
72                //   value bytes (if not NULL)
73                let bytes = value.as_bytes()?;
74                if bytes.len() < 4 {
75                    return Err(Error::from("HSTORE binary data too short"));
76                }
77
78                let mut buf = &bytes[..];
79                use byteorder::{BigEndian, ReadBytesExt};
80
81                let count = buf.read_i32::<BigEndian>()? as usize;
82                let mut map = HashMap::new();
83
84                for _ in 0..count {
85                    if buf.len() < 8 {
86                        return Err(Error::from("HSTORE binary entry too short"));
87                    }
88
89                    let key_len = buf.read_i32::<BigEndian>()? as usize;
90                    let val_len = buf.read_i32::<BigEndian>()? as i32;
91
92                    if buf.len() < key_len {
93                        return Err(Error::from("HSTORE binary key too short"));
94                    }
95
96                    let key = String::from_utf8(buf[..key_len].to_vec())
97                        .map_err(|e| Error::from(format!("Invalid HSTORE key: {}", e)))?;
98                    buf = &buf[key_len..];
99
100                    if val_len < 0 {
101                        // NULL value
102                        map.insert(key, "null".to_string());
103                    } else {
104                        let val_len = val_len as usize;
105                        if buf.len() < val_len {
106                            return Err(Error::from("HSTORE binary value too short"));
107                        }
108
109                        let val = String::from_utf8(buf[..val_len].to_vec())
110                            .map_err(|e| Error::from(format!("Invalid HSTORE value: {}", e)))?;
111                        buf = &buf[val_len..];
112
113                        map.insert(key, val);
114                    }
115                }
116
117                Self(map)
118            }
119            PgValueFormat::Text => {
120                // Text format: "key1=>value1, key2=>value2"
121                let s = value.as_str()?.trim();
122                if s.is_empty() {
123                    return Ok(Self(HashMap::new()));
124                }
125
126                let mut map = HashMap::new();
127                // Parse pairs separated by comma
128                for pair in s.split(',') {
129                    let pair = pair.trim();
130                    if pair.is_empty() {
131                        continue;
132                    }
133
134                    // Find the => separator
135                    if let Some(pos) = pair.find("=>") {
136                        let key = pair[..pos].trim().to_string();
137                        let value = pair[pos + 2..].trim().to_string();
138                        map.insert(key, value);
139                    } else {
140                        return Err(Error::from(format!(
141                            "Invalid HSTORE format: '{}'. Expected 'key=>value'",
142                            pair
143                        )));
144                    }
145                }
146
147                Self(map)
148            }
149        })
150    }
151}
152
153impl Encode for Hstore {
154    fn encode(self, _buf: &mut crate::arguments::PgArgumentBuffer) -> Result<IsNull, Error> {
155        // HSTORE encoding is complex
156        // Applications should use hstore(text) or hstore(text, text) in their query
157        Err(Error::from(
158            "HStore encoding not supported. Use hstore(text) or hstore(text, text) in your query instead."
159        ))
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166    use crate::types::decode::Decode;
167    use crate::value::{PgValue, PgValueFormat};
168
169    #[test]
170    fn test_display() {
171        let mut map = HashMap::new();
172        map.insert("name".to_string(), "John".to_string());
173        map.insert("age".to_string(), "30".to_string());
174        let hstore = Hstore(map);
175        let display = format!("{}", hstore);
176        // HashMap iteration order is not guaranteed, so just check both parts are present
177        assert!(display.contains("name=>John"));
178        assert!(display.contains("age=>30"));
179    }
180
181    #[test]
182    fn test_default() {
183        let hstore = Hstore::default();
184        assert_eq!(hstore.0.len(), 0);
185    }
186
187    #[test]
188    fn test_from_hashmap() {
189        let mut map = HashMap::new();
190        map.insert("key".to_string(), "value".to_string());
191        let hstore: Hstore = map.into();
192        assert_eq!(hstore.0.get("key"), Some(&"value".to_string()));
193    }
194
195    #[test]
196    fn test_decode_text_empty() {
197        let hstore: Hstore = Decode::decode(PgValue {
198            value: Some(b"".to_vec()),
199            type_info: crate::type_info::PgTypeInfo::HSTORE,
200            format: PgValueFormat::Text,
201            timezone_sec: None,
202        }).unwrap();
203        assert_eq!(hstore.0.len(), 0);
204    }
205
206    #[test]
207    fn test_decode_text_single() {
208        let s = "name=>John";
209        let hstore: Hstore = Decode::decode(PgValue {
210            value: Some(s.as_bytes().to_vec()),
211            type_info: crate::type_info::PgTypeInfo::HSTORE,
212            format: PgValueFormat::Text,
213            timezone_sec: None,
214        }).unwrap();
215        assert_eq!(hstore.0.get("name"), Some(&"John".to_string()));
216    }
217
218    #[test]
219    fn test_decode_text_multiple() {
220        let s = "name=>John, age=>30, city=>NYC";
221        let hstore: Hstore = Decode::decode(PgValue {
222            value: Some(s.as_bytes().to_vec()),
223            type_info: crate::type_info::PgTypeInfo::HSTORE,
224            format: PgValueFormat::Text,
225            timezone_sec: None,
226        }).unwrap();
227        assert_eq!(hstore.0.len(), 3);
228        assert_eq!(hstore.0.get("name"), Some(&"John".to_string()));
229        assert_eq!(hstore.0.get("age"), Some(&"30".to_string()));
230        assert_eq!(hstore.0.get("city"), Some(&"NYC".to_string()));
231    }
232
233    #[test]
234    fn test_from_value() {
235        let mut map = HashMap::new();
236        map.insert("key".to_string(), "value".to_string());
237        let hstore = Hstore(map);
238        let value: Value = hstore.into();
239        match value {
240            Value::Ext(type_name, boxed) => {
241                assert_eq!(type_name, "hstore");
242                if let Value::String(s) = *boxed {
243                    assert!(s.contains("key"));
244                    assert!(s.contains("value"));
245                } else {
246                    panic!("Expected String");
247                }
248            }
249            _ => panic!("Expected Ext variant"),
250        }
251    }
252
253    #[test]
254    fn test_equality() {
255        let mut map1 = HashMap::new();
256        map1.insert("key".to_string(), "value".to_string());
257        let h1 = Hstore(map1.clone());
258
259        let h2 = Hstore(map1);
260
261        assert_eq!(h1, h2);
262    }
263}