1use std::path::Path;
4
5use crate::api::Table;
6use crate::database::{ViewDef, is_database_dir, load_database_config, save_database_config};
7use crate::errors::{MdqlError, ValidationError};
8use crate::model::Row;
9use crate::query_engine::{execute_join_query, execute_query};
10use crate::query_parser::{Statement, parse_query};
11
12#[derive(Debug)]
13pub enum QueryResult {
14 Rows { rows: Vec<Row>, columns: Vec<String> },
15 Message(String),
16}
17
18pub fn execute(path: &Path, sql: &str) -> crate::errors::Result<(QueryResult, Vec<ValidationError>)> {
19 let stmt = parse_query(sql)?;
20 let is_db = is_database_dir(path);
21
22 match stmt {
23 Statement::Select(ref q) => {
24 if !q.joins.is_empty() || is_db {
25 let (_config, tables, errors) = crate::loader::load_database(path)?;
26 let (rows, cols) = if !q.joins.is_empty() {
27 execute_join_query(q, &tables)?
28 } else {
29 let (schema, rows) = tables.get(&q.table).ok_or_else(|| {
30 MdqlError::QueryExecution(format!(
31 "table '{}' not found in database",
32 q.table
33 ))
34 })?;
35 execute_query(q, rows, schema)?
36 };
37 Ok((QueryResult::Rows { rows, columns: cols }, errors))
38 } else {
39 let (schema, rows, errors) = crate::loader::load_table(path)?;
40 let (rows, cols) = execute_query(q, &rows, &schema)?;
41 Ok((QueryResult::Rows { rows, columns: cols }, errors))
42 }
43 }
44 Statement::CreateView(ref cv) => {
45 if !is_db {
46 return Err(MdqlError::QueryExecution(
47 "CREATE VIEW requires a database directory".into(),
48 ));
49 }
50 let mut config = load_database_config(path)?;
51
52 let (_config_check, tables, _errors) = crate::loader::load_database(path)?;
53 if tables.contains_key(&cv.view_name) {
54 return Err(MdqlError::QueryExecution(format!(
55 "Name '{}' already exists as a table or view",
56 cv.view_name
57 )));
58 }
59
60 if config.views.iter().any(|v| v.name == cv.view_name) {
61 return Err(MdqlError::QueryExecution(format!(
62 "View '{}' already exists",
63 cv.view_name
64 )));
65 }
66
67 let query_str = sql
68 .to_uppercase()
69 .find(" AS ")
70 .map(|pos| sql[pos + 4..].trim().to_string())
71 .ok_or_else(|| {
72 MdqlError::QueryExecution("CREATE VIEW must contain AS clause".into())
73 })?;
74
75 let view_def = ViewDef {
76 name: cv.view_name.clone(),
77 query: query_str,
78 };
79
80 let test_result = crate::loader::load_database(path);
81 if let Ok((_cfg, test_tables, _errs)) = test_result {
82 let test_view = ViewDef {
83 name: view_def.name.clone(),
84 query: view_def.query.clone(),
85 };
86 if let Err(e) = super::loader::materialize_view(&test_view, &test_tables) {
87 return Err(MdqlError::QueryExecution(format!(
88 "View query failed validation: {}",
89 e
90 )));
91 }
92 }
93
94 config.views.push(view_def);
95 save_database_config(path, &config)?;
96 Ok((
97 QueryResult::Message(format!("View '{}' created", cv.view_name)),
98 vec![],
99 ))
100 }
101 Statement::DropView(ref dv) => {
102 if !is_db {
103 return Err(MdqlError::QueryExecution(
104 "DROP VIEW requires a database directory".into(),
105 ));
106 }
107 let mut config = load_database_config(path)?;
108 let len_before = config.views.len();
109 config.views.retain(|v| v.name != dv.view_name);
110 if config.views.len() == len_before {
111 return Err(MdqlError::QueryExecution(format!(
112 "View '{}' does not exist",
113 dv.view_name
114 )));
115 }
116 save_database_config(path, &config)?;
117 Ok((
118 QueryResult::Message(format!("View '{}' dropped", dv.view_name)),
119 vec![],
120 ))
121 }
122 ref stmt @ (Statement::Insert(_)
123 | Statement::Update(_)
124 | Statement::Delete(_)
125 | Statement::AlterRename(_)
126 | Statement::AlterDrop(_)
127 | Statement::AlterMerge(_)) => {
128 if is_db {
129 let config = load_database_config(path)?;
130 let target = stmt.table_name();
131 if config.views.iter().any(|v| v.name == target) {
132 return Err(MdqlError::QueryExecution(format!(
133 "Cannot write to view '{}' — views are read-only",
134 target
135 )));
136 }
137 }
138 let table_path = if is_db {
139 path.join(stmt.table_name())
140 } else {
141 path.to_path_buf()
142 };
143 let mut table = Table::new(&table_path)?;
144 let msg = table.execute_sql(sql)?;
145 Ok((QueryResult::Message(msg), vec![]))
146 }
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use std::fs;
154
155 fn make_test_db() -> tempfile::TempDir {
156 let dir = tempfile::tempdir().unwrap();
157
158 fs::write(
160 dir.path().join("_mdql.md"),
161 "---\ntype: database\nname: testdb\n---\n",
162 )
163 .unwrap();
164
165 let strats = dir.path().join("strategies");
167 fs::create_dir(&strats).unwrap();
168 fs::write(
169 strats.join("_mdql.md"),
170 "---\ntype: schema\ntable: strategies\nprimary_key: path\nfrontmatter:\n title:\n type: string\n status:\n type: string\n---\n",
171 )
172 .unwrap();
173 fs::write(
174 strats.join("alpha.md"),
175 "---\ntitle: Alpha\nstatus: LIVE\n---\n# Alpha\n",
176 )
177 .unwrap();
178 fs::write(
179 strats.join("beta.md"),
180 "---\ntitle: Beta\nstatus: DRAFT\n---\n# Beta\n",
181 )
182 .unwrap();
183
184 dir
185 }
186
187 #[test]
188 fn test_create_and_query_view() {
189 let dir = make_test_db();
190 let (result, _) = execute(
191 dir.path(),
192 "CREATE VIEW live AS SELECT * FROM strategies WHERE status = 'LIVE'",
193 )
194 .unwrap();
195 assert!(matches!(result, QueryResult::Message(ref m) if m.contains("created")));
196
197 let (result, _) = execute(dir.path(), "SELECT * FROM live").unwrap();
198 if let QueryResult::Rows { rows, columns } = result {
199 assert_eq!(rows.len(), 1);
200 assert!(columns.contains(&"title".to_string()));
201 } else {
202 panic!("Expected Rows");
203 }
204 }
205
206 #[test]
207 fn test_drop_view() {
208 let dir = make_test_db();
209 execute(
210 dir.path(),
211 "CREATE VIEW live AS SELECT * FROM strategies WHERE status = 'LIVE'",
212 )
213 .unwrap();
214
215 let (result, _) = execute(dir.path(), "DROP VIEW live").unwrap();
216 assert!(matches!(result, QueryResult::Message(ref m) if m.contains("dropped")));
217
218 let err = execute(dir.path(), "SELECT * FROM live");
219 assert!(err.is_err());
220 }
221
222 #[test]
223 fn test_drop_nonexistent_view() {
224 let dir = make_test_db();
225 let err = execute(dir.path(), "DROP VIEW nonexistent");
226 assert!(err.is_err());
227 assert!(err.unwrap_err().to_string().contains("does not exist"));
228 }
229
230 #[test]
231 fn test_create_view_duplicate_name() {
232 let dir = make_test_db();
233 execute(
234 dir.path(),
235 "CREATE VIEW live AS SELECT * FROM strategies WHERE status = 'LIVE'",
236 )
237 .unwrap();
238
239 let err = execute(
240 dir.path(),
241 "CREATE VIEW live AS SELECT * FROM strategies",
242 );
243 assert!(err.is_err());
244 assert!(err.unwrap_err().to_string().contains("already exists"));
245 }
246
247 #[test]
248 fn test_create_view_conflicts_with_table() {
249 let dir = make_test_db();
250 let err = execute(
251 dir.path(),
252 "CREATE VIEW strategies AS SELECT * FROM strategies",
253 );
254 assert!(err.is_err());
255 assert!(err.unwrap_err().to_string().contains("already exists"));
256 }
257
258 #[test]
259 fn test_write_to_view_rejected() {
260 let dir = make_test_db();
261 execute(
262 dir.path(),
263 "CREATE VIEW live AS SELECT * FROM strategies WHERE status = 'LIVE'",
264 )
265 .unwrap();
266
267 let err = execute(
268 dir.path(),
269 "INSERT INTO live (title, status) VALUES ('Gamma', 'LIVE')",
270 );
271 assert!(err.is_err());
272 assert!(err.unwrap_err().to_string().contains("read-only"));
273 }
274
275 #[test]
276 fn test_create_view_not_database() {
277 let dir = tempfile::tempdir().unwrap();
278 fs::write(
279 dir.path().join("_mdql.md"),
280 "---\ntype: schema\ntable: t\nprimary_key: path\nfrontmatter:\n x:\n type: string\n---\n",
281 )
282 .unwrap();
283
284 let err = execute(
285 dir.path(),
286 "CREATE VIEW v AS SELECT * FROM t",
287 );
288 assert!(err.is_err());
289 assert!(err.unwrap_err().to_string().contains("database directory"));
290 }
291}