firebolt/
parser.rs

1use crate::error::FireboltError;
2use crate::result::ResultSet;
3use crate::types::{Column, Type};
4use regex::Regex;
5
6fn parse_type(type_str: &str) -> Result<(Type, bool, Option<i32>, Option<i32>), FireboltError> {
7    let is_nullable = type_str.ends_with(" null");
8    let clean_type = if is_nullable {
9        &type_str[..type_str.len() - 5]
10    } else {
11        type_str
12    };
13
14    if let Ok(decimal_regex) = Regex::new(r"(?i)decimal\((\d+),\s*(\d+)\)") {
15        if let Some(captures) = decimal_regex.captures(clean_type) {
16            let precision = captures[1]
17                .parse()
18                .map_err(|_| FireboltError::Query("Invalid decimal precision".to_string()))?;
19            let scale = captures[2]
20                .parse()
21                .map_err(|_| FireboltError::Query("Invalid decimal scale".to_string()))?;
22            return Ok((Type::Decimal, is_nullable, Some(precision), Some(scale)));
23        }
24    }
25
26    if clean_type.starts_with("array") {
27        return Ok((Type::Array, is_nullable, None, None));
28    }
29
30    let base_type = match clean_type {
31        "int" => Type::Int,
32        "bigint" | "long" => Type::Long,
33        "float4" | "float" => Type::Float,
34        "double" | "float8" => Type::Double,
35        "decimal" => Type::Decimal,
36        "text" | "string" => Type::Text,
37        "date" => Type::Date,
38        "timestamp" => Type::Timestamp,
39        "timestamptz" => Type::TimestampTZ,
40        "bool" | "boolean" => Type::Boolean,
41        "bytea" => Type::Bytes,
42        "geography" => Type::Geography,
43        _ if clean_type.starts_with("struct") => Type::Struct,
44        _ => {
45            return Err(FireboltError::Query(format!(
46                "Unsupported type: {clean_type}"
47            )))
48        }
49    };
50
51    Ok((base_type, is_nullable, None, None))
52}
53
54pub fn parse_columns(json: &serde_json::Value) -> Result<Vec<Column>, FireboltError> {
55    let meta = json.get("meta").and_then(|m| m.as_array()).ok_or_else(|| {
56        FireboltError::Query("Missing or invalid 'meta' field in response".to_string())
57    })?;
58
59    meta.iter()
60        .map(|col| {
61            let name = col
62                .get("name")
63                .and_then(|n| n.as_str())
64                .ok_or_else(|| FireboltError::Query("Missing column name".to_string()))?
65                .to_string();
66
67            let type_str = col
68                .get("type")
69                .and_then(|t| t.as_str())
70                .ok_or_else(|| FireboltError::Query("Missing column type".to_string()))?;
71
72            let (r#type, is_nullable, precision, scale) = parse_type(type_str)?;
73
74            Ok(Column {
75                name,
76                r#type,
77                precision,
78                scale,
79                is_nullable,
80            })
81        })
82        .collect()
83}
84
85pub fn parse_data(
86    json: &serde_json::Value,
87    columns: &[Column],
88) -> Result<Vec<crate::result::Row>, FireboltError> {
89    let data = json.get("data").and_then(|d| d.as_array()).ok_or_else(|| {
90        FireboltError::Query("Missing or invalid 'data' field in response".to_string())
91    })?;
92
93    data.iter()
94        .map(|row_array| {
95            let row_values: Vec<serde_json::Value> = row_array
96                .as_array()
97                .ok_or_else(|| FireboltError::Query("Row data is not an array".to_string()))?
98                .to_vec();
99
100            Ok(crate::result::Row::new(row_values, columns.to_vec()))
101        })
102        .collect()
103}
104
105pub fn parse_response(body: String) -> Result<ResultSet, FireboltError> {
106    let json: serde_json::Value = serde_json::from_str(&body)
107        .map_err(|e| FireboltError::Serialization(format!("Failed to parse JSON: {e}")))?;
108
109    let columns = parse_columns(&json)?;
110    let rows = parse_data(&json, &columns)?;
111
112    Ok(ResultSet { columns, rows })
113}
114
115pub fn parse_server_error(body: String) -> FireboltError {
116    FireboltError::Query(format!("Server error: {body}"))
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use crate::types::Type;
123
124    #[test]
125    fn test_parse_type_basic_types() {
126        assert_eq!(parse_type("int").unwrap(), (Type::Int, false, None, None));
127        assert_eq!(
128            parse_type("bigint").unwrap(),
129            (Type::Long, false, None, None)
130        );
131        assert_eq!(parse_type("long").unwrap(), (Type::Long, false, None, None));
132        assert_eq!(
133            parse_type("float").unwrap(),
134            (Type::Float, false, None, None)
135        );
136        assert_eq!(
137            parse_type("float4").unwrap(),
138            (Type::Float, false, None, None)
139        );
140        assert_eq!(
141            parse_type("double").unwrap(),
142            (Type::Double, false, None, None)
143        );
144        assert_eq!(
145            parse_type("float8").unwrap(),
146            (Type::Double, false, None, None)
147        );
148        assert_eq!(parse_type("text").unwrap(), (Type::Text, false, None, None));
149        assert_eq!(
150            parse_type("string").unwrap(),
151            (Type::Text, false, None, None)
152        );
153        assert_eq!(parse_type("date").unwrap(), (Type::Date, false, None, None));
154        assert_eq!(
155            parse_type("timestamp").unwrap(),
156            (Type::Timestamp, false, None, None)
157        );
158        assert_eq!(
159            parse_type("timestamptz").unwrap(),
160            (Type::TimestampTZ, false, None, None)
161        );
162        assert_eq!(
163            parse_type("bool").unwrap(),
164            (Type::Boolean, false, None, None)
165        );
166        assert_eq!(
167            parse_type("boolean").unwrap(),
168            (Type::Boolean, false, None, None)
169        );
170        assert_eq!(
171            parse_type("bytea").unwrap(),
172            (Type::Bytes, false, None, None)
173        );
174        assert_eq!(
175            parse_type("geography").unwrap(),
176            (Type::Geography, false, None, None)
177        );
178        assert_eq!(
179            parse_type("array(int)").unwrap(),
180            (Type::Array, false, None, None)
181        );
182    }
183
184    #[test]
185    fn test_parse_type_nullable() {
186        assert_eq!(
187            parse_type("int null").unwrap(),
188            (Type::Int, true, None, None)
189        );
190        assert_eq!(
191            parse_type("text null").unwrap(),
192            (Type::Text, true, None, None)
193        );
194        assert_eq!(
195            parse_type("array(int) null").unwrap(),
196            (Type::Array, true, None, None)
197        );
198    }
199
200    #[test]
201    fn test_parse_type_decimal_with_precision_scale() {
202        assert_eq!(
203            parse_type("decimal(38, 30)").unwrap(),
204            (Type::Decimal, false, Some(38), Some(30))
205        );
206        assert_eq!(
207            parse_type("decimal(10, 2) null").unwrap(),
208            (Type::Decimal, true, Some(10), Some(2))
209        );
210        assert_eq!(
211            parse_type("Decimal(38, 30)").unwrap(),
212            (Type::Decimal, false, Some(38), Some(30))
213        );
214        assert_eq!(
215            parse_type("Decimal(10, 2) null").unwrap(),
216            (Type::Decimal, true, Some(10), Some(2))
217        );
218    }
219
220    #[test]
221    fn test_parse_type_unsupported() {
222        assert!(parse_type("unsupported_type").is_err());
223    }
224
225    #[test]
226    fn test_parse_columns() {
227        let json = serde_json::json!({
228            "meta": [
229                {"name": "id", "type": "int"},
230                {"name": "name", "type": "text"},
231                {"name": "price", "type": "decimal(10, 2)"},
232                {"name": "nullable_field", "type": "text null"}
233            ]
234        });
235
236        let columns = parse_columns(&json).unwrap();
237        assert_eq!(columns.len(), 4);
238
239        assert_eq!(columns[0].name, "id");
240        assert_eq!(columns[0].r#type, Type::Int);
241        assert!(!columns[0].is_nullable);
242
243        assert_eq!(columns[1].name, "name");
244        assert_eq!(columns[1].r#type, Type::Text);
245        assert!(!columns[1].is_nullable);
246
247        assert_eq!(columns[2].name, "price");
248        assert_eq!(columns[2].r#type, Type::Decimal);
249        assert_eq!(columns[2].precision, Some(10));
250        assert_eq!(columns[2].scale, Some(2));
251        assert!(!columns[2].is_nullable);
252
253        assert_eq!(columns[3].name, "nullable_field");
254        assert_eq!(columns[3].r#type, Type::Text);
255        assert!(columns[3].is_nullable);
256    }
257
258    #[test]
259    fn test_parse_data() {
260        let columns = vec![
261            Column {
262                name: "id".to_string(),
263                r#type: Type::Int,
264                precision: None,
265                scale: None,
266                is_nullable: false,
267            },
268            Column {
269                name: "name".to_string(),
270                r#type: Type::Text,
271                precision: None,
272                scale: None,
273                is_nullable: false,
274            },
275        ];
276
277        let json = serde_json::json!({
278            "data": [
279                [1, "test"],
280                [2, "example"]
281            ]
282        });
283
284        let rows = parse_data(&json, &columns).unwrap();
285        assert_eq!(rows.len(), 2);
286    }
287
288    #[test]
289    fn test_parse_response_success() {
290        let json_response = r#"{
291            "meta": [
292                {"name": "id", "type": "int"},
293                {"name": "name", "type": "text"}
294            ],
295            "data": [
296                [1, "test"],
297                [2, "example"]
298            ],
299            "rows": 2,
300            "statistics": {"elapsed": 0.006947, "rows_read": 2, "bytes_read": 10}
301        }"#;
302
303        let result = parse_response(json_response.to_string());
304        assert!(result.is_ok());
305
306        let result_set = result.unwrap();
307        assert_eq!(result_set.columns.len(), 2);
308        assert_eq!(result_set.rows.len(), 2);
309        assert_eq!(result_set.columns[0].name, "id");
310        assert_eq!(result_set.columns[0].r#type, Type::Int);
311        assert_eq!(result_set.columns[1].name, "name");
312        assert_eq!(result_set.columns[1].r#type, Type::Text);
313    }
314
315    #[test]
316    fn test_parse_response_invalid_json() {
317        let invalid_json = "invalid json";
318
319        let result = parse_response(invalid_json.to_string());
320        assert!(result.is_err());
321        assert!(matches!(
322            result.unwrap_err(),
323            FireboltError::Serialization(_)
324        ));
325    }
326
327    #[test]
328    fn test_parse_response_missing_meta() {
329        let json_response = r#"{"data": []}"#;
330
331        let result = parse_response(json_response.to_string());
332        assert!(result.is_err());
333        assert!(matches!(result.unwrap_err(), FireboltError::Query(_)));
334    }
335
336    #[test]
337    fn test_parse_response_missing_data() {
338        let json_response = r#"{"meta": []}"#;
339
340        let result = parse_response(json_response.to_string());
341        assert!(result.is_err());
342        assert!(matches!(result.unwrap_err(), FireboltError::Query(_)));
343    }
344
345    #[test]
346    fn test_parse_server_error() {
347        let error_body = "Internal Server Error".to_string();
348        let result = parse_server_error(error_body);
349        assert!(matches!(result, FireboltError::Query(_)));
350        assert!(format!("{result:?}").contains("Server error: Internal Server Error"));
351    }
352}