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