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.
39pub fn from_sqlite_value(value: ValueRef<'_>) -> JsonValue {
40    match value {
41        ValueRef::Null => JsonValue::Null,
42        ValueRef::Integer(i) => JsonValue::Number(i.into()),
43        ValueRef::Real(f) => serde_json::Number::from_f64(f)
44            .map(JsonValue::Number)
45            .unwrap_or(JsonValue::Null),
46        ValueRef::Text(bytes) => {
47            let s = String::from_utf8_lossy(bytes).to_string();
48            // Try to parse as JSON
49            if s.starts_with('{') || s.starts_with('[') {
50                serde_json::from_str(&s).unwrap_or(JsonValue::String(s))
51            } else {
52                JsonValue::String(s)
53            }
54        }
55        ValueRef::Blob(bytes) => {
56            // Try to decode as UTF-8 string first
57            match std::str::from_utf8(bytes) {
58                Ok(s) => JsonValue::String(s.to_string()),
59                Err(_) => {
60                    // Binary data, encode as base64
61                    JsonValue::String(base64_encode(bytes))
62                }
63            }
64        }
65    }
66}
67
68/// Get a JSON value from a row at the given column index.
69pub fn get_value_at_index(row: &rusqlite::Row<'_>, index: usize) -> JsonValue {
70    // Try to get the value as each type
71    if let Ok(v) = row.get_ref(index) {
72        from_sqlite_value(v)
73    } else {
74        JsonValue::Null
75    }
76}
77
78/// Simple base64 encoding for binary data.
79fn base64_encode(data: &[u8]) -> String {
80    const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
81
82    let mut result = String::new();
83    let mut i = 0;
84
85    while i < data.len() {
86        let b0 = data[i];
87        let b1 = data.get(i + 1).copied().unwrap_or(0);
88        let b2 = data.get(i + 2).copied().unwrap_or(0);
89
90        result.push(ALPHABET[(b0 >> 2) as usize] as char);
91        result.push(ALPHABET[(((b0 & 0x03) << 4) | (b1 >> 4)) as usize] as char);
92
93        if i + 1 < data.len() {
94            result.push(ALPHABET[(((b1 & 0x0f) << 2) | (b2 >> 6)) as usize] as char);
95        } else {
96            result.push('=');
97        }
98
99        if i + 2 < data.len() {
100            result.push(ALPHABET[(b2 & 0x3f) as usize] as char);
101        } else {
102            result.push('=');
103        }
104
105        i += 3;
106    }
107
108    result
109}
110
111/// A newtype wrapper for JSON values stored in SQLite.
112#[derive(Debug, Clone)]
113pub struct JsonColumn(pub JsonValue);
114
115impl FromSql for JsonColumn {
116    fn column_result(value: ValueRef<'_>) -> Result<Self, FromSqlError> {
117        match value {
118            ValueRef::Text(bytes) => {
119                let s = std::str::from_utf8(bytes).map_err(|e| FromSqlError::Other(Box::new(e)))?;
120                let json: JsonValue =
121                    serde_json::from_str(s).map_err(|e| FromSqlError::Other(Box::new(e)))?;
122                Ok(JsonColumn(json))
123            }
124            ValueRef::Null => Ok(JsonColumn(JsonValue::Null)),
125            _ => Err(FromSqlError::InvalidType),
126        }
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn test_filter_value_to_sqlite_null() {
136        let result = filter_value_to_sqlite(&FilterValue::Null);
137        assert!(matches!(result, Value::Null));
138    }
139
140    #[test]
141    fn test_filter_value_to_sqlite_bool() {
142        let result = filter_value_to_sqlite(&FilterValue::Bool(true));
143        assert!(matches!(result, Value::Integer(1)));
144
145        let result = filter_value_to_sqlite(&FilterValue::Bool(false));
146        assert!(matches!(result, Value::Integer(0)));
147    }
148
149    #[test]
150    fn test_filter_value_to_sqlite_int() {
151        let result = filter_value_to_sqlite(&FilterValue::Int(42));
152        assert!(matches!(result, Value::Integer(42)));
153    }
154
155    #[test]
156    #[allow(clippy::approx_constant)]
157    fn test_filter_value_to_sqlite_float() {
158        let result = filter_value_to_sqlite(&FilterValue::Float(3.14));
159        match result {
160            Value::Real(f) => assert!((f - 3.14).abs() < f64::EPSILON),
161            _ => panic!("Expected Real"),
162        }
163    }
164
165    #[test]
166    fn test_filter_value_to_sqlite_string() {
167        let result = filter_value_to_sqlite(&FilterValue::String("hello".to_string()));
168        assert!(matches!(result, Value::Text(s) if s == "hello"));
169    }
170
171    #[test]
172    fn test_from_sqlite_value_null() {
173        let result = from_sqlite_value(ValueRef::Null);
174        assert_eq!(result, JsonValue::Null);
175    }
176
177    #[test]
178    fn test_from_sqlite_value_integer() {
179        let result = from_sqlite_value(ValueRef::Integer(42));
180        assert_eq!(result, JsonValue::Number(42.into()));
181    }
182
183    #[test]
184    #[allow(clippy::approx_constant)]
185    fn test_from_sqlite_value_real() {
186        let result = from_sqlite_value(ValueRef::Real(3.14));
187        if let JsonValue::Number(n) = result {
188            assert!((n.as_f64().unwrap() - 3.14).abs() < f64::EPSILON);
189        } else {
190            panic!("Expected Number");
191        }
192    }
193
194    #[test]
195    fn test_from_sqlite_value_text() {
196        let result = from_sqlite_value(ValueRef::Text(b"hello"));
197        assert_eq!(result, JsonValue::String("hello".to_string()));
198    }
199
200    #[test]
201    fn test_from_sqlite_value_json_text() {
202        let result = from_sqlite_value(ValueRef::Text(b"{\"key\": \"value\"}"));
203        if let JsonValue::Object(map) = result {
204            assert_eq!(
205                map.get("key"),
206                Some(&JsonValue::String("value".to_string()))
207            );
208        } else {
209            panic!("Expected Object");
210        }
211    }
212
213    #[test]
214    fn test_base64_encode() {
215        let result = base64_encode(b"Hello");
216        assert_eq!(result, "SGVsbG8=");
217    }
218}