1use crossbeam_channel::Sender;
2use std::{fs, path::PathBuf};
3use tracing::{debug, error, info};
4
5use crate::error::MqAdapterError;
6use crate::protocol::DebuggerMessage;
7
8type DynResult<T> = miette::Result<T, Box<dyn std::error::Error>>;
9
10pub fn execute_query(
12 mut engine: mq_lang::DefaultEngine,
13 query: String,
14 input_file: Option<String>,
15 message_tx: Sender<DebuggerMessage>,
16) -> DynResult<()> {
17 debug!(query = %query, input_file = ?input_file, "Executing query in background thread");
18
19 let query = fs::read_to_string(&query).map_err(|e| {
20 let error_msg = format!("Failed to read query file '{}': {}", query, e);
21 error!(error = %error_msg);
22 Box::new(MqAdapterError::FileError(error_msg)) as Box<dyn std::error::Error>
23 })?;
24
25 let input_data = if let Some(file_path) = input_file {
27 let input = fs::read_to_string(&file_path).map_err(|e| {
28 let error_msg = format!("Failed to read input file '{}': {}", file_path, e);
29 error!(error = %error_msg);
30 Box::new(MqAdapterError::FileError(error_msg)) as Box<dyn std::error::Error>
31 })?;
32
33 parse_input_data(&file_path, &input)?
34 } else {
35 mq_lang::null_input()
36 };
37
38 let result = engine.eval(&query, input_data.into_iter());
39
40 match result {
41 Ok(values) => {
42 let output = values
43 .values()
44 .iter()
45 .map(|v| v.to_string())
46 .collect::<Vec<_>>()
47 .join("\n");
48 info!(output = %output, "Query execution completed successfully");
49 }
50 Err(e) => {
51 let error_msg = format!("Query execution failed: {}", e);
52 error!(error = %error_msg);
53 let _ = message_tx.send(DebuggerMessage::Terminated);
55 return Err(Box::new(MqAdapterError::QueryError(error_msg)));
56 }
57 }
58
59 if let Err(e) = message_tx.send(DebuggerMessage::Terminated) {
60 error!(error = %e, "Failed to send terminated message");
61 }
62
63 Ok(())
64}
65
66fn parse_input_data(file_path: &str, input: &str) -> DynResult<Vec<mq_lang::RuntimeValue>> {
68 match PathBuf::from(file_path)
69 .extension()
70 .unwrap_or_default()
71 .to_string_lossy()
72 .to_lowercase()
73 .as_str()
74 {
75 "json" | "csv" | "tsv" | "xml" | "toml" | "yaml" | "yml" | "txt" => Ok(mq_lang::raw_input(input)),
76 "html" | "htm" => mq_lang::parse_html_input(input).map_err(|e| {
77 let error_msg = format!("Failed to parse input file '{}': {}", file_path, e);
78 error!(error = %error_msg);
79 Box::new(MqAdapterError::FileError(error_msg)) as Box<dyn std::error::Error>
80 }),
81 "mdx" => mq_lang::parse_mdx_input(input).map_err(|e| {
82 let error_msg = format!("Failed to parse input file '{}': {}", file_path, e);
83 error!(error = %error_msg);
84 Box::new(MqAdapterError::FileError(error_msg)) as Box<dyn std::error::Error>
85 }),
86 _ => mq_lang::parse_markdown_input(input).map_err(|e| {
87 let error_msg = format!("Failed to parse input file '{}': {}", file_path, e);
88 error!(error = %error_msg);
89 Box::new(MqAdapterError::FileError(error_msg)) as Box<dyn std::error::Error>
90 }),
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use crossbeam_channel::unbounded;
98 use std::fs;
99 use tempfile::TempDir;
100
101 fn dummy_engine() -> mq_lang::DefaultEngine {
103 mq_lang::DefaultEngine::default()
104 }
105
106 #[test]
107 fn test_parse_input_data_json() {
108 let input = r#"{"key": "value"}"#;
109 let result = parse_input_data("test.json", input).unwrap();
110 assert!(!result.is_empty(), "Should parse JSON input as raw");
111 }
112
113 #[test]
114 fn test_parse_input_data_csv() {
115 let input = "name,age\nJohn,30\nJane,25";
116 let result = parse_input_data("test.csv", input).unwrap();
117 assert!(!result.is_empty());
118 }
119
120 #[test]
121 fn test_parse_input_data_tsv() {
122 let input = "name\tage\nJohn\t30\nJane\t25";
123 let result = parse_input_data("test.tsv", input).unwrap();
124 assert!(!result.is_empty());
125 }
126
127 #[test]
128 fn test_parse_input_data_xml() {
129 let input = "<root><item>test</item></root>";
130 let result = parse_input_data("test.xml", input).unwrap();
131 assert!(!result.is_empty());
132 }
133
134 #[test]
135 fn test_parse_input_data_toml() {
136 let input = r#"
137[package]
138name = "test"
139"#;
140 let result = parse_input_data("test.toml", input).unwrap();
141 assert!(!result.is_empty());
142 }
143
144 #[test]
145 fn test_parse_input_data_yaml() {
146 let input = r#"
147name: test
148version: 1.0.0
149"#;
150 let result = parse_input_data("test.yaml", input).unwrap();
151 assert!(!result.is_empty());
152 }
153
154 #[test]
155 fn test_parse_input_data_txt() {
156 let input = "This is plain text";
157 let result = parse_input_data("test.txt", input).unwrap();
158 assert!(!result.is_empty());
159 }
160
161 #[test]
162 fn test_parse_input_data_html() {
163 let input = r#"<div>Hello</div>"#;
164 let result = parse_input_data("test.html", input);
165 assert!(result.is_ok(), "Should parse HTML input");
166
167 let input = "<html><body><h1>Hello</h1></body></html>";
168 let result = parse_input_data("test.html", input);
169 assert!(result.is_ok() || result.is_err());
172 }
173
174 #[test]
175 fn test_parse_input_data_htm() {
176 let input = "<html><body><h1>Hello</h1></body></html>";
177 let result = parse_input_data("test.htm", input);
178 assert!(result.is_ok() || result.is_err());
180 }
181
182 #[test]
183 fn test_parse_input_data_mdx() {
184 let input = r#"# Hello MDX"#;
185 let result = parse_input_data("test.mdx", input);
186 assert!(result.is_ok(), "Should parse MDX input");
187
188 let input = "# Hello\n\n```js\nconsole.log('hello');\n```";
189 let result = parse_input_data("test.mdx", input);
190 assert!(result.is_ok() || result.is_err());
192 }
193
194 #[test]
195 fn test_parse_input_data_markdown_default() {
196 let input = r#"# Hello Markdown"#;
197 let result = parse_input_data("test.unknown", input);
198 assert!(result.is_ok(), "Should parse unknown extension as Markdown");
199 let input = "# Hello World\n\nThis is markdown content.";
200 let result = parse_input_data("test.md", input);
201 assert!(result.is_ok() || result.is_err());
203 }
204
205 #[test]
206 fn test_parse_input_data_unknown_extension() {
207 let input = "# Hello World\n\nThis is treated as markdown.";
208 let result = parse_input_data("test.unknown", input);
209 assert!(result.is_ok() || result.is_err());
211 }
212
213 #[test]
214 fn test_execute_query_success() {
215 let engine = dummy_engine();
216 let query_file = "test_query.txt";
217 let input_file = None;
218 let (tx, rx) = unbounded();
219
220 std::fs::write(query_file, ".h").unwrap();
222
223 let result = execute_query(engine, query_file.to_string(), input_file, tx);
224 assert!(result.is_ok(), "Query execution should succeed");
225 assert!(matches!(rx.recv().unwrap(), DebuggerMessage::Terminated));
226
227 let _ = std::fs::remove_file(query_file);
229 }
230
231 #[test]
232 fn test_execute_query_query_file_not_found() {
233 let engine = dummy_engine();
234 let query_file = "non_existent_query.txt";
235 let input_file = None;
236 let (tx, _rx) = unbounded();
237
238 let result = execute_query(engine, query_file.to_string(), input_file, tx);
239 assert!(result.is_err(), "Should error if query file does not exist");
240 let temp_dir = TempDir::new().unwrap();
241 let query_file_path = temp_dir.path().join("test.mq");
242 let input_file_path = temp_dir.path().join("input.md");
243
244 fs::write(&query_file_path, "# Simple Query\n.").unwrap();
246
247 fs::write(&input_file_path, "# Test Input\n\nThis is a test.").unwrap();
249
250 let engine = mq_lang::DefaultEngine::default();
251 let (tx, _rx) = unbounded::<DebuggerMessage>();
252
253 let result = execute_query(
254 engine,
255 query_file_path.to_string_lossy().to_string(),
256 Some(input_file_path.to_string_lossy().to_string()),
257 tx,
258 );
259
260 assert!(result.is_ok() || result.is_err());
263 }
264
265 #[test]
266 fn test_execute_query_without_input_file() {
267 let temp_dir = TempDir::new().unwrap();
268 let query_file_path = temp_dir.path().join("test.mq");
269
270 fs::write(&query_file_path, ".").unwrap();
272
273 let engine = mq_lang::DefaultEngine::default();
274 let (tx, _rx) = unbounded::<DebuggerMessage>();
275
276 let result = execute_query(engine, query_file_path.to_string_lossy().to_string(), None, tx);
277
278 assert!(result.is_ok() || result.is_err());
280 }
281
282 #[test]
283 fn test_execute_query_nonexistent_query_file() {
284 let engine = mq_lang::DefaultEngine::default();
285 let (tx, _rx) = unbounded::<DebuggerMessage>();
286
287 let result = execute_query(engine, "nonexistent.mq".to_string(), None, tx);
288
289 assert!(result.is_err());
290 assert!(result.unwrap_err().to_string().contains("Failed to read query file"));
291 }
292
293 #[test]
294 fn test_execute_query_nonexistent_input_file() {
295 let temp_dir = TempDir::new().unwrap();
296 let query_file_path = temp_dir.path().join("test.mq");
297
298 fs::write(&query_file_path, ".").unwrap();
300
301 let engine = mq_lang::DefaultEngine::default();
302 let (tx, _rx) = unbounded::<DebuggerMessage>();
303
304 let result = execute_query(
305 engine,
306 query_file_path.to_string_lossy().to_string(),
307 Some("nonexistent_input.md".to_string()),
308 tx,
309 );
310
311 assert!(result.is_err());
312 assert!(result.unwrap_err().to_string().contains("Failed to read input file"));
313 }
314
315 #[test]
316 fn test_execute_query_sends_terminated_message() {
317 let temp_dir = TempDir::new().unwrap();
318 let query_file_path = temp_dir.path().join("test.mq");
319
320 fs::write(&query_file_path, ".").unwrap();
322
323 let engine = mq_lang::DefaultEngine::default();
324 let (tx, rx) = unbounded::<DebuggerMessage>();
325
326 let result = execute_query(engine, query_file_path.to_string_lossy().to_string(), None, tx);
327
328 if result.is_ok() {
330 if let Ok(message) = rx.try_recv() {
332 assert!(matches!(message, DebuggerMessage::Terminated));
333 }
334 } else {
335 if let Ok(message) = rx.try_recv() {
337 assert!(matches!(message, DebuggerMessage::Terminated));
338 }
339 }
340 }
341
342 #[test]
343 fn test_parse_input_data_edge_cases() {
344 let result = parse_input_data("", "content");
346 assert!(result.is_ok() || result.is_err());
347
348 let result = parse_input_data("filename", "content");
350 assert!(result.is_ok() || result.is_err());
351
352 let result = parse_input_data("file.name.with.dots.md", "# Content");
354 assert!(result.is_ok() || result.is_err());
355
356 let result = parse_input_data("test.JSON", r#"{"key": "value"}"#);
358 assert!(!result.unwrap().is_empty());
359 }
360}