sql_cli/
dynamic_schema.rs

1use crate::app_paths::AppPaths;
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::fs;
6use std::path::PathBuf;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct TableInfo {
10    pub name: String,
11    pub description: Option<String>,
12    pub row_count: Option<usize>,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ColumnInfo {
17    pub name: String,
18    #[serde(rename = "type")]
19    pub data_type: String,
20    pub nullable: bool,
21    pub description: Option<String>,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct TableSchema {
26    pub table_name: String,
27    pub columns: Vec<ColumnInfo>,
28    pub methods: HashMap<String, Vec<String>>,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct SchemaResponse {
33    pub tables: Vec<TableInfo>,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct AllSchemasResponse {
38    pub schemas: HashMap<String, TableSchema>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct CachedSchema {
43    pub schemas: HashMap<String, TableSchema>,
44    pub last_updated: DateTime<Utc>,
45    pub server_url: String,
46}
47
48pub struct SchemaManager {
49    cache_path: PathBuf,
50    cached_schema: Option<CachedSchema>,
51    api_client: crate::api_client::ApiClient,
52}
53
54impl SchemaManager {
55    pub fn new(api_client: crate::api_client::ApiClient) -> Self {
56        let cache_path = AppPaths::schemas_file().unwrap_or_else(|_| PathBuf::from("schemas.json"));
57
58        Self {
59            cache_path,
60            cached_schema: None,
61            api_client,
62        }
63    }
64
65    pub fn load_schema(
66        &mut self,
67    ) -> Result<HashMap<String, TableSchema>, Box<dyn std::error::Error>> {
68        // Try to fetch from server first
69        if let Ok(schemas) = self.fetch_from_server() {
70            self.save_cache(&schemas)?;
71            return Ok(schemas);
72        }
73
74        // Fall back to cache
75        if let Ok(schemas) = self.load_from_cache() {
76            // Using cached schema (server unavailable)
77            return Ok(schemas);
78        }
79
80        // Last resort: use default schema
81        // Using default schema (no server or cache available)
82        Ok(self.get_default_schema())
83    }
84
85    fn fetch_from_server(
86        &self,
87    ) -> Result<HashMap<String, TableSchema>, Box<dyn std::error::Error>> {
88        // For now, return error to trigger fallback
89        // TODO: Implement actual API call when server endpoint is ready
90        Err("Schema API not yet implemented".into())
91    }
92
93    fn load_from_cache(
94        &mut self,
95    ) -> Result<HashMap<String, TableSchema>, Box<dyn std::error::Error>> {
96        if !self.cache_path.exists() {
97            return Err("No cache file found".into());
98        }
99
100        let content = fs::read_to_string(&self.cache_path)?;
101        let cached: CachedSchema = serde_json::from_str(&content)?;
102
103        // Check if cache is less than 24 hours old
104        let age = Utc::now() - cached.last_updated;
105        if age.num_hours() > 24 {
106            // Warning: Schema cache is old
107        }
108
109        self.cached_schema = Some(cached.clone());
110        Ok(cached.schemas)
111    }
112
113    fn save_cache(
114        &self,
115        schemas: &HashMap<String, TableSchema>,
116    ) -> Result<(), Box<dyn std::error::Error>> {
117        let cached = CachedSchema {
118            schemas: schemas.clone(),
119            last_updated: Utc::now(),
120            server_url: self.api_client.base_url.clone(),
121        };
122
123        // Create directory if it doesn't exist
124        if let Some(parent) = self.cache_path.parent() {
125            fs::create_dir_all(parent)?;
126        }
127
128        let json = serde_json::to_string_pretty(&cached)?;
129        fs::write(&self.cache_path, json)?;
130
131        Ok(())
132    }
133
134    fn get_default_schema(&self) -> HashMap<String, TableSchema> {
135        // Convert from existing schema.json format
136        let config = crate::schema_config::load_schema_config();
137        let mut schemas = HashMap::new();
138
139        for table_config in config.tables {
140            let columns: Vec<ColumnInfo> = table_config
141                .columns
142                .iter()
143                .map(|name| {
144                    ColumnInfo {
145                        name: name.clone(),
146                        data_type: self.infer_type(name),
147                        nullable: true, // Conservative default
148                        description: None,
149                    }
150                })
151                .collect();
152
153            let mut methods = HashMap::new();
154            methods.insert(
155                "string".to_string(),
156                vec![
157                    "Contains".to_string(),
158                    "StartsWith".to_string(),
159                    "EndsWith".to_string(),
160                ],
161            );
162            methods.insert("datetime".to_string(), vec!["DateTime".to_string()]);
163
164            schemas.insert(
165                table_config.name.clone(),
166                TableSchema {
167                    table_name: table_config.name,
168                    columns,
169                    methods,
170                },
171            );
172        }
173
174        schemas
175    }
176
177    fn infer_type(&self, column_name: &str) -> String {
178        // Simple type inference based on column name
179        if column_name.ends_with("Date") || column_name.ends_with("Time") {
180            "datetime".to_string()
181        } else if column_name.ends_with("Id") || column_name.ends_with("Name") {
182            "string".to_string()
183        } else if column_name == "price"
184            || column_name == "quantity"
185            || column_name == "commission"
186            || column_name.ends_with("Amount")
187        {
188            "decimal".to_string()
189        } else {
190            "string".to_string()
191        }
192    }
193
194    pub fn get_tables(&self) -> Vec<String> {
195        if let Some(ref cached) = self.cached_schema {
196            cached.schemas.keys().cloned().collect()
197        } else {
198            vec!["trade_deal".to_string()]
199        }
200    }
201
202    pub fn get_columns(&self, table: &str) -> Vec<String> {
203        if let Some(ref cached) = self.cached_schema {
204            if let Some(schema) = cached.schemas.get(table) {
205                return schema.columns.iter().map(|c| c.name.clone()).collect();
206            }
207        }
208
209        // Fallback to default
210        self.get_default_schema()
211            .get(table)
212            .map(|s| s.columns.iter().map(|c| c.name.clone()).collect())
213            .unwrap_or_default()
214    }
215
216    pub fn refresh_schema(&mut self) -> Result<(), Box<dyn std::error::Error>> {
217        let schemas = self.fetch_from_server()?;
218        self.save_cache(&schemas)?;
219        Ok(())
220    }
221}