Skip to main content

supabase_client_core/
value.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value as JsonValue;
3#[cfg(feature = "direct-sql")]
4use sqlx::Row as SqlxRow;
5use std::collections::HashMap;
6use std::ops::{Deref, DerefMut};
7
8/// A dynamic row type wrapping a HashMap of column name to JSON value.
9/// Used for the string-based (dynamic) API when not using typed structs.
10#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
11pub struct Row(pub HashMap<String, JsonValue>);
12
13impl Row {
14    /// Create an empty row.
15    pub fn new() -> Self {
16        Self(HashMap::new())
17    }
18
19    /// Create a row with pre-allocated capacity.
20    pub fn with_capacity(capacity: usize) -> Self {
21        Self(HashMap::with_capacity(capacity))
22    }
23
24    /// Set a column value.
25    pub fn set(&mut self, key: impl Into<String>, value: impl Into<JsonValue>) -> &mut Self {
26        self.0.insert(key.into(), value.into());
27        self
28    }
29
30    /// Get a column value.
31    pub fn get_value(&self, key: &str) -> Option<&JsonValue> {
32        self.0.get(key)
33    }
34
35    /// Check if a column exists.
36    pub fn contains(&self, key: &str) -> bool {
37        self.0.contains_key(key)
38    }
39
40    /// Get a typed value from a column, returning None if missing or wrong type.
41    pub fn get_as<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
42        self.0
43            .get(key)
44            .and_then(|v| serde_json::from_value(v.clone()).ok())
45    }
46
47    /// Get column names.
48    pub fn columns(&self) -> Vec<&str> {
49        self.0.keys().map(|k| k.as_str()).collect()
50    }
51
52    /// Get the number of columns.
53    pub fn len(&self) -> usize {
54        self.0.len()
55    }
56
57    /// Check if the row is empty.
58    pub fn is_empty(&self) -> bool {
59        self.0.is_empty()
60    }
61
62    /// Consume the row and return the inner HashMap.
63    pub fn into_inner(self) -> HashMap<String, JsonValue> {
64        self.0
65    }
66}
67
68impl Deref for Row {
69    type Target = HashMap<String, JsonValue>;
70    fn deref(&self) -> &Self::Target {
71        &self.0
72    }
73}
74
75impl DerefMut for Row {
76    fn deref_mut(&mut self) -> &mut Self::Target {
77        &mut self.0
78    }
79}
80
81impl<K: Into<String>, V: Into<JsonValue>> FromIterator<(K, V)> for Row {
82    fn from_iter<I: IntoIterator<Item = (K, V)>>(iter: I) -> Self {
83        let map = iter
84            .into_iter()
85            .map(|(k, v)| (k.into(), v.into()))
86            .collect();
87        Self(map)
88    }
89}
90
91impl<K: Into<String>, V: Into<JsonValue>, const N: usize> From<[(K, V); N]> for Row {
92    fn from(arr: [(K, V); N]) -> Self {
93        arr.into_iter().collect()
94    }
95}
96
97#[cfg(feature = "direct-sql")]
98impl<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow> for Row {
99    fn from_row(row: &'r sqlx::postgres::PgRow) -> Result<Self, sqlx::Error> {
100        use sqlx::Column;
101        let mut map = Row::new();
102        for col in row.columns() {
103            let name = col.name();
104            // Try types in order of likelihood
105            if let Ok(v) = row.try_get::<JsonValue, _>(name) {
106                map.set(name, v);
107            } else if let Ok(v) = row.try_get::<String, _>(name) {
108                map.set(name, JsonValue::String(v));
109            } else if let Ok(v) = row.try_get::<i64, _>(name) {
110                map.set(name, JsonValue::Number(v.into()));
111            } else if let Ok(v) = row.try_get::<i32, _>(name) {
112                map.set(name, JsonValue::Number(v.into()));
113            } else if let Ok(v) = row.try_get::<f64, _>(name) {
114                if let Some(n) = serde_json::Number::from_f64(v) {
115                    map.set(name, JsonValue::Number(n));
116                } else {
117                    map.set(name, JsonValue::Null);
118                }
119            } else if let Ok(v) = row.try_get::<bool, _>(name) {
120                map.set(name, JsonValue::Bool(v));
121            } else if let Ok(v) = row.try_get::<uuid::Uuid, _>(name) {
122                map.set(name, JsonValue::String(v.to_string()));
123            } else if let Ok(v) = row.try_get::<chrono::NaiveDateTime, _>(name) {
124                map.set(name, JsonValue::String(v.to_string()));
125            } else if let Ok(v) = row.try_get::<chrono::DateTime<chrono::Utc>, _>(name) {
126                map.set(name, JsonValue::String(v.to_rfc3339()));
127            } else {
128                map.set(name, JsonValue::Null);
129            }
130        }
131        Ok(map)
132    }
133}
134
135/// Macro for constructing a `Row` with key-value pairs.
136///
137/// # Examples
138/// ```
139/// use supabase_client_core::row;
140/// let row = row![("name", "Auckland"), ("country_id", 554)];
141/// ```
142#[macro_export]
143macro_rules! row {
144    () => {
145        $crate::Row::new()
146    };
147    ($(($key:expr, $val:expr)),+ $(,)?) => {{
148        let mut row = $crate::Row::new();
149        $(
150            row.set($key, serde_json::json!($val));
151        )+
152        row
153    }};
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn test_row_new() {
162        let row = Row::new();
163        assert!(row.is_empty());
164    }
165
166    #[test]
167    fn test_row_set_get() {
168        let mut row = Row::new();
169        row.set("name", JsonValue::String("Auckland".to_string()));
170        assert_eq!(
171            row.get_value("name"),
172            Some(&JsonValue::String("Auckland".to_string()))
173        );
174        assert!(row.contains("name"));
175        assert!(!row.contains("missing"));
176    }
177
178    #[test]
179    fn test_row_macro() {
180        let row = row![("name", "Auckland"), ("id", 1)];
181        assert_eq!(row.len(), 2);
182        assert!(row.contains("name"));
183        assert!(row.contains("id"));
184    }
185
186    #[test]
187    fn test_row_get_as() {
188        let row = row![("count", 42)];
189        assert_eq!(row.get_as::<i64>("count"), Some(42));
190        assert_eq!(row.get_as::<String>("count"), None);
191    }
192}