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