sql_cli/data/adapters/
csv_client_adapter.rs

1//! Adapter to make `CsvApiClient` implement `DataProvider` trait
2//!
3//! This adapter allows the existing `CsvApiClient` to work with the new `DataProvider`
4//! trait system without modifying the `CsvApiClient` code itself.
5
6use crate::api_client::QueryResponse;
7use crate::data::csv_datasource::CsvApiClient;
8use crate::data::data_provider::DataProvider;
9use std::fmt::Debug;
10
11/// Adapter that makes `CsvApiClient` implement `DataProvider`
12/// Note: This adapter requires querying the data first since `CsvApiClient`
13/// doesn't store results internally - it generates them on query
14pub struct CsvClientAdapter<'a> {
15    client: &'a CsvApiClient,
16    cached_response: Option<QueryResponse>,
17}
18
19impl<'a> CsvClientAdapter<'a> {
20    /// Create a new `CsvClientAdapter` wrapping a `CsvApiClient`
21    /// You should call `execute_query()` to populate data before using `DataProvider` methods
22    #[must_use]
23    pub fn new(client: &'a CsvApiClient) -> Self {
24        Self {
25            client,
26            cached_response: None,
27        }
28    }
29
30    /// Execute a query and cache the results for `DataProvider` access
31    pub fn execute_query(&mut self, sql: &str) -> anyhow::Result<()> {
32        let response = self.client.query_csv(sql)?;
33        self.cached_response = Some(response);
34        Ok(())
35    }
36}
37
38impl Debug for CsvClientAdapter<'_> {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        f.debug_struct("CsvClientAdapter")
41            .field("row_count", &self.get_row_count())
42            .field("column_count", &self.get_column_count())
43            .field("has_data", &self.cached_response.is_some())
44            .finish()
45    }
46}
47
48impl DataProvider for CsvClientAdapter<'_> {
49    fn get_row(&self, index: usize) -> Option<Vec<String>> {
50        self.cached_response.as_ref().and_then(|response| {
51            response.data.get(index).map(|json_value| {
52                // Convert JSON value to Vec<String>
53                if let Some(obj) = json_value.as_object() {
54                    // Get column names to ensure consistent ordering
55                    let columns = self.get_column_names();
56                    columns
57                        .iter()
58                        .map(|col| {
59                            obj.get(col)
60                                .map(|v| {
61                                    // Convert JSON value to string
62                                    match v {
63                                        serde_json::Value::String(s) => s.clone(),
64                                        serde_json::Value::Null => String::new(),
65                                        other => other.to_string(),
66                                    }
67                                })
68                                .unwrap_or_default()
69                        })
70                        .collect()
71                } else {
72                    // Fallback for non-object JSON values
73                    vec![json_value.to_string()]
74                }
75            })
76        })
77    }
78
79    fn get_column_names(&self) -> Vec<String> {
80        // Try to get from cached response first
81        if let Some(ref response) = self.cached_response {
82            // Extract column names from first data row if available
83            if let Some(first_row) = response.data.first() {
84                if let Some(obj) = first_row.as_object() {
85                    return obj.keys().map(std::string::ToString::to_string).collect();
86                }
87            }
88        }
89
90        // Fallback to schema if no data
91        self.client
92            .get_schema()
93            .and_then(|schema| schema.values().next().cloned())
94            .unwrap_or_default()
95    }
96
97    fn get_row_count(&self) -> usize {
98        self.cached_response.as_ref().map_or(0, |r| r.data.len())
99    }
100
101    fn get_column_count(&self) -> usize {
102        self.get_column_names().len()
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use crate::data::csv_datasource::CsvApiClient;
110    use serde_json::json;
111    use std::fs;
112    use tempfile::tempdir;
113
114    #[test]
115    fn test_csv_client_adapter_basic() {
116        // Create a CsvApiClient with test data
117        let temp_dir = tempdir().unwrap();
118        let json_path = temp_dir.path().join("test_data.json");
119
120        let test_data = json!([
121            {
122                "id": 1,
123                "name": "Alice",
124                "age": 30
125            },
126            {
127                "id": 2,
128                "name": "Bob",
129                "age": 25
130            },
131            {
132                "id": 3,
133                "name": "Charlie",
134                "age": 35
135            }
136        ]);
137
138        fs::write(&json_path, serde_json::to_string(&test_data).unwrap()).unwrap();
139
140        let mut client = CsvApiClient::new();
141        client.load_json(&json_path, "test").unwrap();
142
143        // Create adapter and execute query
144        let mut adapter = CsvClientAdapter::new(&client);
145        adapter.execute_query("SELECT * FROM test").unwrap();
146
147        // Test DataProvider methods
148        assert_eq!(adapter.get_row_count(), 3);
149        assert_eq!(adapter.get_column_count(), 3);
150
151        let col_names = adapter.get_column_names();
152        assert!(col_names.contains(&"id".to_string()));
153        assert!(col_names.contains(&"name".to_string()));
154        assert!(col_names.contains(&"age".to_string()));
155
156        // Test getting rows
157        let row = adapter.get_row(0).unwrap();
158        assert!(row.contains(&"1".to_string()));
159        assert!(row.contains(&"Alice".to_string()));
160        assert!(row.contains(&"30".to_string()));
161
162        let row = adapter.get_row(2).unwrap();
163        assert!(row.contains(&"3".to_string()));
164        assert!(row.contains(&"Charlie".to_string()));
165        assert!(row.contains(&"35".to_string()));
166
167        // Test out of bounds
168        assert!(adapter.get_row(3).is_none());
169    }
170
171    #[test]
172    fn test_csv_client_adapter_empty() {
173        let client = CsvApiClient::new();
174        let adapter = CsvClientAdapter::new(&client);
175
176        // Without executing a query, should return empty data
177        assert_eq!(adapter.get_row_count(), 0);
178        assert_eq!(adapter.get_column_count(), 0);
179        assert!(adapter.get_row(0).is_none());
180    }
181
182    #[test]
183    fn test_csv_client_adapter_with_filter() {
184        // Create a CsvApiClient with test data
185        let temp_dir = tempdir().unwrap();
186        let json_path = temp_dir.path().join("test_data.json");
187
188        let test_data = json!([
189            {
190                "id": 1,
191                "name": "Alice",
192                "status": "active"
193            },
194            {
195                "id": 2,
196                "name": "Bob",
197                "status": "inactive"
198            },
199            {
200                "id": 3,
201                "name": "Charlie",
202                "status": "active"
203            }
204        ]);
205
206        fs::write(&json_path, serde_json::to_string(&test_data).unwrap()).unwrap();
207
208        let mut client = CsvApiClient::new();
209        client.load_json(&json_path, "test").unwrap();
210
211        // Create adapter and execute filtered query
212        let mut adapter = CsvClientAdapter::new(&client);
213        adapter
214            .execute_query("SELECT * FROM test WHERE status = 'active'")
215            .unwrap();
216
217        // Should only have 2 active rows
218        assert_eq!(adapter.get_row_count(), 2);
219
220        // Check that both rows are active status
221        for i in 0..adapter.get_row_count() {
222            let row = adapter.get_row(i).unwrap();
223            assert!(row.contains(&"active".to_string()));
224        }
225    }
226}