Skip to main content

prax_sqlite/
types.rs

1//! Type conversion utilities for SQLite.
2
3use rusqlite::types::{FromSql, FromSqlError, Value, ValueRef};
4use serde_json::Value as JsonValue;
5
6use prax_query::filter::FilterValue;
7
8/// Convert a FilterValue to a SQLite Value.
9pub fn filter_value_to_sqlite(value: &FilterValue) -> Value {
10    match value {
11        FilterValue::Null => Value::Null,
12        FilterValue::Bool(b) => Value::Integer(i64::from(*b)),
13        FilterValue::Int(i) => Value::Integer(*i),
14        FilterValue::Float(f) => Value::Real(*f),
15        FilterValue::String(s) => Value::Text(s.clone()),
16        FilterValue::Json(j) => Value::Text(j.to_string()),
17        FilterValue::List(list) => {
18            // Serialize list as JSON
19            let json_array: Vec<JsonValue> = list
20                .iter()
21                .map(|v| match v {
22                    FilterValue::Null => JsonValue::Null,
23                    FilterValue::Bool(b) => JsonValue::Bool(*b),
24                    FilterValue::Int(i) => JsonValue::Number((*i).into()),
25                    FilterValue::Float(f) => serde_json::Number::from_f64(*f)
26                        .map(JsonValue::Number)
27                        .unwrap_or(JsonValue::Null),
28                    FilterValue::String(s) => JsonValue::String(s.clone()),
29                    FilterValue::Json(j) => j.clone(),
30                    FilterValue::List(_) => JsonValue::Null, // Nested lists not directly supported
31                })
32                .collect();
33            Value::Text(serde_json::to_string(&json_array).unwrap_or_default())
34        }
35    }
36}
37
38/// Convert a SQLite ValueRef to a JSON Value.
39///
40/// # JSON sniffing
41///
42/// This is a raw JSON path with no schema information, so TEXT values
43/// are sniffed: text beginning with `{` or `[` is parsed as JSON in
44/// this raw JSON path (falling back to the plain string if parsing
45/// fails). A stored string that legitimately starts with one of those
46/// characters is therefore returned as parsed JSON rather than as the
47/// stored string.
48pub fn from_sqlite_value(value: ValueRef<'_>) -> JsonValue {
49    match value {
50        ValueRef::Null => JsonValue::Null,
51        ValueRef::Integer(i) => JsonValue::Number(i.into()),
52        ValueRef::Real(f) => serde_json::Number::from_f64(f)
53            .map(JsonValue::Number)
54            .unwrap_or(JsonValue::Null),
55        ValueRef::Text(bytes) => {
56            let s = String::from_utf8_lossy(bytes).to_string();
57            // Try to parse as JSON
58            if s.starts_with('{') || s.starts_with('[') {
59                serde_json::from_str(&s).unwrap_or(JsonValue::String(s))
60            } else {
61                JsonValue::String(s)
62            }
63        }
64        ValueRef::Blob(bytes) => {
65            // Try to decode as UTF-8 string first
66            match std::str::from_utf8(bytes) {
67                Ok(s) => JsonValue::String(s.to_string()),
68                Err(_) => {
69                    // Binary data, encode as base64
70                    JsonValue::String(base64_encode(bytes))
71                }
72            }
73        }
74    }
75}
76
77/// Get a JSON value from a row at the given column index.
78///
79/// # Null fallback
80///
81/// This function is infallible: any `get_ref` failure (e.g. an
82/// out-of-range column index) is silently mapped to
83/// [`JsonValue::Null`] rather than propagated.
84///
85/// Text beginning with `{` or `[` is parsed as JSON in this raw JSON
86/// path — see [`from_sqlite_value`] for details.
87pub fn get_value_at_index(row: &rusqlite::Row<'_>, index: usize) -> JsonValue {
88    // Try to get the value as each type
89    if let Ok(v) = row.get_ref(index) {
90        from_sqlite_value(v)
91    } else {
92        JsonValue::Null
93    }
94}
95
96/// Simple base64 encoding for binary data.
97fn base64_encode(data: &[u8]) -> String {
98    const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
99
100    let mut result = String::new();
101    let mut i = 0;
102
103    while i < data.len() {
104        let b0 = data[i];
105        let b1 = data.get(i + 1).copied().unwrap_or(0);
106        let b2 = data.get(i + 2).copied().unwrap_or(0);
107
108        result.push(ALPHABET[(b0 >> 2) as usize] as char);
109        result.push(ALPHABET[(((b0 & 0x03) << 4) | (b1 >> 4)) as usize] as char);
110
111        if i + 1 < data.len() {
112            result.push(ALPHABET[(((b1 & 0x0f) << 2) | (b2 >> 6)) as usize] as char);
113        } else {
114            result.push('=');
115        }
116
117        if i + 2 < data.len() {
118            result.push(ALPHABET[(b2 & 0x3f) as usize] as char);
119        } else {
120            result.push('=');
121        }
122
123        i += 3;
124    }
125
126    result
127}
128
129/// A newtype wrapper for JSON values stored in SQLite.
130#[derive(Debug, Clone)]
131pub struct JsonColumn(pub JsonValue);
132
133impl FromSql for JsonColumn {
134    fn column_result(value: ValueRef<'_>) -> Result<Self, FromSqlError> {
135        match value {
136            ValueRef::Text(bytes) => {
137                let s = std::str::from_utf8(bytes).map_err(|e| FromSqlError::Other(Box::new(e)))?;
138                let json: JsonValue =
139                    serde_json::from_str(s).map_err(|e| FromSqlError::Other(Box::new(e)))?;
140                Ok(JsonColumn(json))
141            }
142            ValueRef::Null => Ok(JsonColumn(JsonValue::Null)),
143            _ => Err(FromSqlError::InvalidType),
144        }
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn test_filter_value_to_sqlite_null() {
154        let result = filter_value_to_sqlite(&FilterValue::Null);
155        assert!(matches!(result, Value::Null));
156    }
157
158    #[test]
159    fn test_filter_value_to_sqlite_bool() {
160        let result = filter_value_to_sqlite(&FilterValue::Bool(true));
161        assert!(matches!(result, Value::Integer(1)));
162
163        let result = filter_value_to_sqlite(&FilterValue::Bool(false));
164        assert!(matches!(result, Value::Integer(0)));
165    }
166
167    #[test]
168    fn test_filter_value_to_sqlite_int() {
169        let result = filter_value_to_sqlite(&FilterValue::Int(42));
170        assert!(matches!(result, Value::Integer(42)));
171    }
172
173    #[test]
174    #[allow(clippy::approx_constant)]
175    fn test_filter_value_to_sqlite_float() {
176        let result = filter_value_to_sqlite(&FilterValue::Float(3.14));
177        match result {
178            Value::Real(f) => assert!((f - 3.14).abs() < f64::EPSILON),
179            _ => panic!("Expected Real"),
180        }
181    }
182
183    #[test]
184    fn test_filter_value_to_sqlite_string() {
185        let result = filter_value_to_sqlite(&FilterValue::String("hello".to_string()));
186        assert!(matches!(result, Value::Text(s) if s == "hello"));
187    }
188
189    #[test]
190    fn test_from_sqlite_value_null() {
191        let result = from_sqlite_value(ValueRef::Null);
192        assert_eq!(result, JsonValue::Null);
193    }
194
195    #[test]
196    fn test_from_sqlite_value_integer() {
197        let result = from_sqlite_value(ValueRef::Integer(42));
198        assert_eq!(result, JsonValue::Number(42.into()));
199    }
200
201    #[test]
202    #[allow(clippy::approx_constant)]
203    fn test_from_sqlite_value_real() {
204        let result = from_sqlite_value(ValueRef::Real(3.14));
205        if let JsonValue::Number(n) = result {
206            assert!((n.as_f64().unwrap() - 3.14).abs() < f64::EPSILON);
207        } else {
208            panic!("Expected Number");
209        }
210    }
211
212    #[test]
213    fn test_from_sqlite_value_text() {
214        let result = from_sqlite_value(ValueRef::Text(b"hello"));
215        assert_eq!(result, JsonValue::String("hello".to_string()));
216    }
217
218    #[test]
219    fn test_from_sqlite_value_json_text() {
220        let result = from_sqlite_value(ValueRef::Text(b"{\"key\": \"value\"}"));
221        if let JsonValue::Object(map) = result {
222            assert_eq!(
223                map.get("key"),
224                Some(&JsonValue::String("value".to_string()))
225            );
226        } else {
227            panic!("Expected Object");
228        }
229    }
230
231    #[test]
232    fn test_base64_encode() {
233        let result = base64_encode(b"Hello");
234        assert_eq!(result, "SGVsbG8=");
235    }
236}