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 #[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 if let Ok(schemas) = self.fetch_from_server() {
71 self.save_cache(&schemas)?;
72 return Ok(schemas);
73 }
74
75 if let Ok(schemas) = self.load_from_cache() {
77 return Ok(schemas);
79 }
80
81 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 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 let age = Utc::now() - cached.last_updated;
106 if age.num_hours() > 24 {
107 }
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 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 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, 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 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 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}