1use std::collections::HashMap;
4use std::path::PathBuf;
5use std::sync::{Arc, Mutex};
6
7use axum::extract::{Path as AxumPath, State};
8use axum::http::{header, StatusCode, Uri};
9use axum::response::{IntoResponse, Json};
10use axum::routing::{get, post};
11use axum::Router;
12use rust_embed::Embed;
13use serde::{Deserialize, Serialize};
14use tower_http::cors::CorsLayer;
15
16use mdql_core::api::Table;
17use mdql_core::loader;
18use mdql_core::model::{Row, Value};
19use mdql_core::projector::format_results;
20use mdql_core::query_parser::{parse_query, Statement};
21use mdql_core::schema::Schema;
22
23#[derive(Embed)]
24#[folder = "static/"]
25struct StaticFiles;
26
27#[derive(Clone)]
28struct AppState {
29 db_path: PathBuf,
30 tables: Arc<Mutex<HashMap<String, (Schema, Vec<Row>)>>>,
31 fk_errors: Arc<Mutex<Vec<mdql_core::errors::ValidationError>>>,
32}
33
34#[derive(Serialize)]
35struct TableInfo {
36 name: String,
37 row_count: usize,
38}
39
40#[derive(Serialize)]
41struct TablesResponse {
42 tables: Vec<TableInfo>,
43}
44
45#[derive(Serialize)]
46struct TableDetailResponse {
47 table: String,
48 primary_key: String,
49 row_count: usize,
50 frontmatter: HashMap<String, FieldInfo>,
51 sections: HashMap<String, SectionInfo>,
52}
53
54#[derive(Serialize)]
55struct FieldInfo {
56 #[serde(rename = "type")]
57 field_type: String,
58 required: bool,
59 enum_values: Option<Vec<String>>,
60}
61
62#[derive(Serialize)]
63struct SectionInfo {
64 content_type: String,
65 required: bool,
66}
67
68#[derive(Deserialize)]
69struct QueryRequest {
70 sql: String,
71 #[serde(default = "default_format")]
72 format: String,
73}
74
75fn default_format() -> String {
76 "table".into()
77}
78
79#[derive(Serialize)]
80struct QueryResponse {
81 columns: Option<Vec<String>>,
82 rows: Option<Vec<HashMap<String, serde_json::Value>>>,
83 output: Option<String>,
84 error: Option<String>,
85 row_count: Option<usize>,
86}
87
88pub async fn run_server(db_path: PathBuf, port: u16) {
89 let tables = match load_all_tables(&db_path) {
91 Ok(t) => t,
92 Err(e) => {
93 eprintln!("Failed to load database: {}", e);
94 std::process::exit(1);
95 }
96 };
97
98 let fk_errors: Arc<Mutex<Vec<mdql_core::errors::ValidationError>>> =
99 Arc::new(Mutex::new(Vec::new()));
100
101 let state = AppState {
102 db_path: db_path.clone(),
103 tables: Arc::new(Mutex::new(tables)),
104 fk_errors: fk_errors.clone(),
105 };
106
107 {
109 let tables_clone = state.tables.clone();
110 let fk_errors_clone = fk_errors.clone();
111 let db_path_clone = db_path.clone();
112 tokio::task::spawn_blocking(move || {
113 let watcher = match mdql_core::watcher::FkWatcher::start(db_path_clone.clone()) {
114 Ok(w) => w,
115 Err(e) => {
116 eprintln!("Warning: could not start FK watcher: {}", e);
117 return;
118 }
119 };
120 loop {
121 if let Some(errors) = watcher.poll() {
122 *fk_errors_clone.lock().unwrap() = errors;
123 if let Ok(new_tables) = load_all_tables(&db_path_clone) {
125 *tables_clone.lock().unwrap() = new_tables;
126 }
127 }
128 std::thread::sleep(std::time::Duration::from_millis(200));
129 }
130 });
131 }
132
133 let app = Router::new()
134 .route("/api/tables", get(list_tables))
135 .route("/api/tables/{name}", get(table_detail))
136 .route("/api/query", post(execute_query))
137 .route("/api/reload", post(reload_tables))
138 .route("/api/fk-errors", get(get_fk_errors))
139 .fallback(static_handler)
140 .layer(CorsLayer::permissive())
141 .with_state(state);
142
143 let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", port))
144 .await
145 .expect("Failed to bind");
146
147 println!("MDQL client running at http://localhost:{}", port);
148
149 axum::serve(listener, app).await.expect("Server failed");
150}
151
152fn load_all_tables(db_path: &std::path::Path) -> Result<HashMap<String, (Schema, Vec<Row>)>, String> {
153 if let Ok((_config, tables, _errors)) = loader::load_database(db_path) {
155 return Ok(tables);
156 }
157
158 match loader::load_table(db_path) {
160 Ok((schema, rows, _errors)) => {
161 let mut map = HashMap::new();
162 map.insert(schema.table.clone(), (schema, rows));
163 Ok(map)
164 }
165 Err(e) => Err(format!("Failed to load: {}", e)),
166 }
167}
168
169async fn list_tables(State(state): State<AppState>) -> Json<TablesResponse> {
170 let tables = state.tables.lock().unwrap();
171 let mut infos: Vec<TableInfo> = tables
172 .iter()
173 .map(|(name, (_schema, rows))| TableInfo {
174 name: name.clone(),
175 row_count: rows.len(),
176 })
177 .collect();
178 infos.sort_by(|a, b| a.name.cmp(&b.name));
179 Json(TablesResponse { tables: infos })
180}
181
182async fn table_detail(
183 State(state): State<AppState>,
184 AxumPath(name): AxumPath<String>,
185) -> Result<Json<TableDetailResponse>, StatusCode> {
186 let tables = state.tables.lock().unwrap();
187 let (schema, rows) = tables.get(&name).ok_or(StatusCode::NOT_FOUND)?;
188
189 let frontmatter: HashMap<String, FieldInfo> = schema
190 .frontmatter
191 .iter()
192 .map(|(k, v)| {
193 (
194 k.clone(),
195 FieldInfo {
196 field_type: format!("{:?}", v.field_type),
197 required: v.required,
198 enum_values: v.enum_values.clone(),
199 },
200 )
201 })
202 .collect();
203
204 let sections: HashMap<String, SectionInfo> = schema
205 .sections
206 .iter()
207 .map(|(k, v)| {
208 (
209 k.clone(),
210 SectionInfo {
211 content_type: v.content_type.clone(),
212 required: v.required,
213 },
214 )
215 })
216 .collect();
217
218 Ok(Json(TableDetailResponse {
219 table: schema.table.clone(),
220 primary_key: schema.primary_key.clone(),
221 row_count: rows.len(),
222 frontmatter,
223 sections,
224 }))
225}
226
227async fn execute_query(
228 State(state): State<AppState>,
229 Json(req): Json<QueryRequest>,
230) -> Json<QueryResponse> {
231 let tables = state.tables.lock().unwrap();
232
233 let stmt = match parse_query(&req.sql) {
235 Ok(s) => s,
236 Err(e) => {
237 return Json(QueryResponse {
238 columns: None,
239 rows: None,
240 output: None,
241 error: Some(format!("Parse error: {}", e)),
242 row_count: None,
243 });
244 }
245 };
246
247 match stmt {
248 Statement::Select(query) => {
249 let table_name = &query.table;
251 let (schema, rows) = match tables.get(table_name.as_str()) {
252 Some(t) => t,
253 None => {
254 return Json(QueryResponse {
255 columns: None,
256 rows: None,
257 output: None,
258 error: Some(format!("Unknown table '{}'", table_name)),
259 row_count: None,
260 });
261 }
262 };
263
264 let result = if !query.joins.is_empty() {
266 mdql_core::query_engine::execute_join_query(&query, &tables)
267 } else {
268 mdql_core::query_engine::execute_query(&query, rows, schema)
269 };
270
271 match result {
272 Ok((result_rows, columns)) => {
273 if req.format == "json" || req.format == "csv" {
274 let output = format_results(
275 &result_rows,
276 Some(&columns),
277 &req.format,
278 80,
279 );
280 Json(QueryResponse {
281 columns: None,
282 rows: None,
283 output: Some(output),
284 error: None,
285 row_count: Some(result_rows.len()),
286 })
287 } else {
288 let json_rows: Vec<HashMap<String, serde_json::Value>> = result_rows
289 .iter()
290 .map(|row| {
291 columns
292 .iter()
293 .map(|col| {
294 let val = row.get(col).unwrap_or(&Value::Null);
295 (col.clone(), value_to_json(val))
296 })
297 .collect()
298 })
299 .collect();
300
301 Json(QueryResponse {
302 columns: Some(columns),
303 rows: Some(json_rows.clone()),
304 output: None,
305 error: None,
306 row_count: Some(json_rows.len()),
307 })
308 }
309 }
310 Err(e) => Json(QueryResponse {
311 columns: None,
312 rows: None,
313 output: None,
314 error: Some(e.to_string()),
315 row_count: None,
316 }),
317 }
318 }
319 _ => {
320 drop(tables); let result = execute_write(&state, &req.sql);
323 Json(QueryResponse {
324 columns: None,
325 rows: None,
326 output: Some(result.clone()),
327 error: if result.starts_with("Error") {
328 Some(result)
329 } else {
330 None
331 },
332 row_count: None,
333 })
334 }
335 }
336}
337
338fn execute_write(state: &AppState, sql: &str) -> String {
339 let stmt = match parse_query(sql) {
341 Ok(s) => s,
342 Err(e) => return format!("Error: {}", e),
343 };
344
345 let table_name = match &stmt {
346 Statement::Insert(q) => q.table.clone(),
347 Statement::Update(q) => q.table.clone(),
348 Statement::Delete(q) => q.table.clone(),
349 Statement::AlterRename(q) => q.table.clone(),
350 Statement::AlterDrop(q) => q.table.clone(),
351 Statement::AlterMerge(q) => q.table.clone(),
352 _ => return "Error: unsupported statement type".into(),
353 };
354
355 let table_path = state.db_path.join(&table_name);
356 let mut table = match Table::new(&table_path) {
357 Ok(t) => t,
358 Err(e) => return format!("Error: {}", e),
359 };
360
361 let result = match table.execute_sql(sql) {
362 Ok(s) => s,
363 Err(e) => format!("Error: {}", e),
364 };
365
366 if let Ok(new_tables) = load_all_tables(&state.db_path) {
368 let mut tables = state.tables.lock().unwrap();
369 *tables = new_tables;
370 }
371
372 result
373}
374
375async fn get_fk_errors(State(state): State<AppState>) -> Json<serde_json::Value> {
376 let errors = state.fk_errors.lock().unwrap();
377 let error_list: Vec<serde_json::Value> = errors
378 .iter()
379 .map(|e| {
380 serde_json::json!({
381 "file": e.file_path,
382 "field": e.field,
383 "message": e.message,
384 })
385 })
386 .collect();
387 Json(serde_json::json!({ "errors": error_list }))
388}
389
390async fn reload_tables(State(state): State<AppState>) -> Json<serde_json::Value> {
391 match load_all_tables(&state.db_path) {
392 Ok(new_tables) => {
393 let mut tables = state.tables.lock().unwrap();
394 *tables = new_tables;
395 Json(serde_json::json!({ "status": "ok" }))
396 }
397 Err(e) => Json(serde_json::json!({ "status": "error", "message": e })),
398 }
399}
400
401fn value_to_json(val: &Value) -> serde_json::Value {
402 match val {
403 Value::Null => serde_json::Value::Null,
404 Value::String(s) => serde_json::Value::String(s.clone()),
405 Value::Int(n) => serde_json::json!(n),
406 Value::Float(f) => serde_json::json!(f),
407 Value::Bool(b) => serde_json::json!(b),
408 Value::Date(d) => serde_json::Value::String(d.format("%Y-%m-%d").to_string()),
409 Value::DateTime(dt) => serde_json::Value::String(dt.format("%Y-%m-%dT%H:%M:%S").to_string()),
410 Value::List(items) => serde_json::json!(items),
411 }
412}
413
414async fn static_handler(uri: Uri) -> impl IntoResponse {
415 let path = uri.path().trim_start_matches('/');
416 let path = if path.is_empty() { "index.html" } else { path };
417
418 match StaticFiles::get(path) {
419 Some(content) => {
420 let mime = mime_guess::from_path(path).first_or_octet_stream();
421 (
422 StatusCode::OK,
423 [(header::CONTENT_TYPE, mime.as_ref().to_string())],
424 content.data.into_owned(),
425 )
426 .into_response()
427 }
428 None => {
429 match StaticFiles::get("index.html") {
431 Some(content) => (
432 StatusCode::OK,
433 [(header::CONTENT_TYPE, "text/html".to_string())],
434 content.data.into_owned(),
435 )
436 .into_response(),
437 None => (StatusCode::NOT_FOUND, "Not found").into_response(),
438 }
439 }
440 }
441}
442