sql_cli/
dynamic_schema.rs1use 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 if let Ok(schemas) = self.fetch_from_server() {
70 self.save_cache(&schemas)?;
71 return Ok(schemas);
72 }
73
74 if let Ok(schemas) = self.load_from_cache() {
76 return Ok(schemas);
78 }
79
80 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 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 let age = Utc::now() - cached.last_updated;
105 if age.num_hours() > 24 {
106 }
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 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 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, 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 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 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}