sql_cli/data/
datasource_adapter.rs

1use crate::api_client::QueryResponse;
2use crate::csv_datasource::CsvApiClient;
3use crate::datasource_trait::{DataSource, DataSourceQueryResponse};
4use anyhow::Result;
5use std::collections::HashMap;
6use tracing::{debug, info, trace};
7
8/// Adapter to make `CsvApiClient` implement `DataSource` trait
9pub struct CsvDataSourceAdapter {
10    client: CsvApiClient,
11}
12
13impl CsvDataSourceAdapter {
14    #[must_use]
15    pub fn new(client: CsvApiClient) -> Self {
16        Self { client }
17    }
18
19    pub fn from_csv_path(path: &str, table_name: &str) -> Result<Self> {
20        info!(target: "datasource", "Loading CSV from path: {}", path);
21        let mut client = CsvApiClient::new();
22        client.load_csv(path, table_name)?;
23
24        // Log metadata about loaded data
25        if let Some(schema) = client.get_schema() {
26            if let Some(columns) = schema.get(table_name) {
27                info!(target: "datasource", "CSV loaded successfully: table='{}', columns={}", 
28                    table_name, columns.len());
29                debug!(target: "datasource", "Column names: {:?}", columns);
30            }
31        }
32
33        Ok(Self { client })
34    }
35
36    pub fn from_json_path(path: &str, table_name: &str) -> Result<Self> {
37        info!(target: "datasource", "Loading JSON from path: {}", path);
38        let mut client = CsvApiClient::new();
39        client.load_json(path, table_name)?;
40
41        // Log metadata about loaded data
42        if let Some(schema) = client.get_schema() {
43            if let Some(columns) = schema.get(table_name) {
44                info!(target: "datasource", "JSON loaded successfully: table='{}', columns={}", 
45                    table_name, columns.len());
46                debug!(target: "datasource", "Column names: {:?}", columns);
47            }
48        }
49
50        Ok(Self { client })
51    }
52
53    /// Get access to the underlying CSV client if needed
54    #[must_use]
55    pub fn inner(&self) -> &CsvApiClient {
56        &self.client
57    }
58
59    pub fn inner_mut(&mut self) -> &mut CsvApiClient {
60        &mut self.client
61    }
62}
63
64impl DataSource for CsvDataSourceAdapter {
65    fn query(&self, sql: &str) -> Result<DataSourceQueryResponse> {
66        debug!(target: "datasource", "Executing query: {}", sql);
67        let response = self.client.query_csv(sql)?;
68        let converted = convert_query_response(response);
69        info!(target: "datasource", "Query returned {} rows with {} columns", 
70            converted.count, converted.columns.len());
71        trace!(target: "datasource", "Query columns: {:?}", converted.columns);
72        Ok(converted)
73    }
74
75    fn query_with_options(
76        &self,
77        sql: &str,
78        case_insensitive: bool,
79    ) -> Result<DataSourceQueryResponse> {
80        // Temporarily set case insensitive for this query
81        let mut temp_client = self.client.clone();
82        temp_client.set_case_insensitive(case_insensitive);
83        let response = temp_client.query_csv(sql)?;
84        Ok(convert_query_response(response))
85    }
86
87    fn get_schema(&self) -> Option<HashMap<String, Vec<String>>> {
88        self.client.get_schema()
89    }
90
91    fn get_table_name(&self) -> String {
92        // Get first table name from schema, or default
93        self.get_schema()
94            .and_then(|schema| schema.keys().next().cloned())
95            .unwrap_or_else(|| "data".to_string())
96    }
97
98    fn get_row_count(&self) -> usize {
99        // This is a bit hacky but works for now
100        // In the future, CsvApiClient should expose row count directly
101        self.query("SELECT * FROM data")
102            .map(|r| r.count)
103            .unwrap_or(0)
104    }
105
106    fn is_case_insensitive(&self) -> bool {
107        // CsvApiClient doesn't expose this, so we track it separately
108        // For now, return false as default
109        false
110    }
111
112    fn set_case_insensitive(&mut self, case_insensitive: bool) {
113        self.client.set_case_insensitive(case_insensitive);
114    }
115
116    fn clone_box(&self) -> Box<dyn DataSource> {
117        Box::new(Self {
118            client: self.client.clone(),
119        })
120    }
121}
122
123/// Convert `CsvApiClient`'s `QueryResponse` to our `DataSourceQueryResponse`
124fn convert_query_response(response: QueryResponse) -> DataSourceQueryResponse {
125    // Extract columns from the first row if available
126    let columns = if let Some(first_row) = response.data.first() {
127        if let Some(obj) = first_row.as_object() {
128            obj.keys().cloned().collect()
129        } else {
130            vec![]
131        }
132    } else {
133        vec![]
134    };
135
136    DataSourceQueryResponse {
137        data: response.data,
138        count: response.count,
139        columns,
140        table_name: "data".to_string(), // Default table name
141    }
142}