1mod ast;
2mod error;
3mod lexer;
4mod parser;
5
6use std::fmt::Display;
7
8pub use ast::*;
9use async_trait::async_trait;
10pub use error::{ParseError, ParseResult};
11use parser::Parser;
12
13pub fn parse_query(query: &str) -> ParseResult<IqlQuery> {
14 let mut parser = Parser::new(query);
15 parser.parse()
16}
17
18#[derive(thiserror::Error, Debug)]
19pub enum IqlError {
20 #[error("IQL query could not be parsed: {0}")]
21 MalformedIql(#[from] ParseError),
22 #[error("Not implemented")]
23 NotImplemented,
24 #[error("This action is not supported by the chosen backend")]
25 NotSupported,
26 #[error("A project with the name '{0}' already exists")]
27 ProjectAlreadyExists(String),
28 #[error("No item of type '{kind}' with the id '{id}' exists")]
29 ItemNotFound { kind: String, id: String },
30 #[error("The issue withe the name '{0}' was already closed. Reason '{1}'")]
31 IssueAlreadyClosed(String, CloseReason),
32 #[error("Field not found: {0}")]
33 FieldNotFound(String),
34 #[error("{0}")]
35 ImplementationSpecific(String),
36}
37
38#[async_trait]
39pub trait ExecutionEngine {
40 async fn execute(&mut self, query: &IqlQuery) -> Result<ExecutionResult, IqlError>;
41}
42
43#[derive(Debug, Clone)]
44pub struct ExecutionResult {
45 pub affected_rows: u128,
46 pub info: Option<String>,
47}
48
49impl Display for ExecutionResult {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 write!(f, "Affected Rows: {}", self.affected_rows)?;
52 if let Some(info) = &self.info {
53 write!(f, "\nInfo: {}", info)?;
54 }
55 Ok(())
56 }
57}
58
59impl From<String> for ExecutionResult {
60 fn from(s: String) -> Self {
61 Self {
62 affected_rows: 0,
63 info: Some(s),
64 }
65 }
66}
67
68impl From<&str> for ExecutionResult {
69 fn from(s: &str) -> Self {
70 Self {
71 affected_rows: 0,
72 info: Some(s.to_string()),
73 }
74 }
75}
76
77impl ExecutionResult {
78 pub fn new(rows: u128) -> Self {
79 Self {
80 affected_rows: rows,
81 info: None,
82 }
83 }
84
85 pub fn one() -> Self {
86 Self {
87 affected_rows: 1,
88 info: None,
89 }
90 }
91
92 pub fn zero() -> Self {
93 Self {
94 affected_rows: 0,
95 info: None,
96 }
97 }
98
99 pub fn with_info(mut self, info: &str) -> Self {
100 self.info = Some(info.to_string());
101 self
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 #[test]
110 fn test_parse_create_user() {
111 let query = "CREATE USER john_doe WITH EMAIL 'john@example.com' NAME 'John Doe'";
112 let result = parse_query(query).unwrap();
113 insta::assert_debug_snapshot!(&result);
114 }
115
116 #[test]
117 fn test_parse_create_project() {
118 let query = "CREATE PROJECT my-project WITH NAME 'My Project' DESCRIPTION 'A test project'";
119 let result = parse_query(query).unwrap();
120 insta::assert_debug_snapshot!(&result);
121 }
122
123 #[test]
124 fn test_parse_create_issue() {
125 let query = "CREATE ISSUE OF KIND bug IN my-project WITH TITLE 'Bug found' DESCRIPTION 'Something broke' PRIORITY high ASSIGNEE john_doe";
126 let result = parse_query(query).unwrap();
127 insta::assert_debug_snapshot!(&result);
128 }
129
130 #[test]
131 fn test_parse_select_all() {
132 let query = "SELECT * FROM issues";
133 let result = parse_query(query).unwrap();
134 insta::assert_debug_snapshot!(&result);
135 }
136
137 #[test]
138 fn test_parse_select_with_where() {
139 let query = "SELECT * FROM issues WHERE status = 'open' AND priority = high";
140 let result = parse_query(query).unwrap();
141 insta::assert_debug_snapshot!(&result);
142 }
143
144 #[test]
145 fn test_parse_update() {
146 let query = "UPDATE issue my-project#123 SET status = 'closed', priority = low";
147 let result = parse_query(query).unwrap();
148 insta::assert_debug_snapshot!(&result);
149 }
150
151 #[test]
152 fn test_parse_delete() {
153 let query = "DELETE issue my-project#456";
154 let result = parse_query(query).unwrap();
155 insta::assert_debug_snapshot!(&result);
156 }
157
158 #[test]
159 fn test_parse_assign() {
160 let query = "ASSIGN issue my-project#789 TO alice";
161 let result = parse_query(query).unwrap();
162 insta::assert_debug_snapshot!(&result);
163 }
164
165 #[test]
166 fn test_parse_close() {
167 let query = "CLOSE issue my-project#101";
168 let result = parse_query(query).unwrap();
169 insta::assert_debug_snapshot!(&result);
170 }
171
172 #[test]
173 fn test_parse_comment() {
174 let query = "COMMENT ON issue my-project#202 WITH 'This is a comment'";
175 let result = parse_query(query).unwrap();
176 insta::assert_debug_snapshot!(&result);
177 }
178
179 #[test]
180 fn test_parse_complex_query() {
181 let query = "SELECT title, status, assignee FROM issues WHERE project = 'backend' AND (priority = high OR status = 'critical') ORDER BY created_at DESC LIMIT 10";
182 let result = parse_query(query).unwrap();
183 insta::assert_debug_snapshot!(&result);
184 }
185
186 #[test]
187 fn test_parse_project_qualified_issue() {
188 let query = "CLOSE issue my-project#42 WITH done";
189 let result = parse_query(query).unwrap();
190 insta::assert_debug_snapshot!(&result);
191 }
192
193 #[test]
194 fn test_parse_multiple_field_updates() {
195 let query = "UPDATE issue my-project#100 SET status = 'closed', priority = medium, assignee = 'bob'";
196 let result = parse_query(query).unwrap();
197 insta::assert_debug_snapshot!(&result);
198 }
199
200 #[test]
201 fn test_parse_in_operator() {
202 let query = "SELECT * FROM issues WHERE priority IN (critical, high)";
203 let result = parse_query(query).unwrap();
204 insta::assert_debug_snapshot!(&result);
205 }
206
207 #[test]
208 fn test_parse_is_null() {
209 let query = "SELECT * FROM issues WHERE assignee IS NULL";
210 let result = parse_query(query).unwrap();
211 insta::assert_debug_snapshot!(&result);
212 }
213
214 #[test]
215 fn test_parse_is_not_null() {
216 let query = "SELECT * FROM issues WHERE assignee IS NOT NULL";
217 let result = parse_query(query).unwrap();
218 insta::assert_debug_snapshot!(&result);
219 }
220
221 #[test]
222 fn test_parse_not_operator() {
223 let query = "SELECT * FROM issues WHERE NOT status = 'closed'";
224 let result = parse_query(query).unwrap();
225 insta::assert_debug_snapshot!(&result);
226 }
227
228 #[test]
229 fn test_parse_like_operator() {
230 let query = "SELECT * FROM issues WHERE title LIKE '%bug%'";
231 let result = parse_query(query).unwrap();
232 insta::assert_debug_snapshot!(&result);
233 }
234
235 #[test]
236 fn test_parse_order_asc() {
237 let query = "SELECT * FROM issues ORDER BY created_at ASC";
238 let result = parse_query(query).unwrap();
239 insta::assert_debug_snapshot!(&result);
240 }
241
242 #[test]
243 fn test_parse_offset() {
244 let query = "SELECT * FROM issues LIMIT 10 OFFSET 20";
245 let result = parse_query(query).unwrap();
246 insta::assert_debug_snapshot!(&result);
247 }
248
249 #[test]
250 fn test_parse_all_priorities() {
251 let queries = vec![
252 "CREATE ISSUE OF KIND bug IN test WITH TITLE 'Test' PRIORITY critical",
253 "CREATE ISSUE OF KIND bug IN test WITH TITLE 'Test' PRIORITY high",
254 "CREATE ISSUE OF KIND bug IN test WITH TITLE 'Test' PRIORITY medium",
255 "CREATE ISSUE OF KIND bug IN test WITH TITLE 'Test' PRIORITY low",
256 ];
257
258 for query in queries {
259 let result = parse_query(query).unwrap();
260 insta::assert_debug_snapshot!(&result);
261 }
262 }
263
264 #[test]
265 fn test_parse_all_entity_types() {
266 let queries = vec![
267 "SELECT * FROM users",
268 "SELECT * FROM projects",
269 "SELECT * FROM issues",
270 "SELECT * FROM comments",
271 ];
272
273 for query in queries {
274 let result = parse_query(query).unwrap();
275 insta::assert_debug_snapshot!(&result);
276 }
277 }
278
279 #[test]
280 fn test_integration_workflow() {
281 let queries = vec![
282 "CREATE USER alice WITH EMAIL 'alice@test.com' NAME 'Alice'",
283 "CREATE PROJECT backend WITH NAME 'Backend' OWNER alice",
284 "CREATE ISSUE OF KIND bug IN backend WITH TITLE 'Bug fix' PRIORITY high ASSIGNEE alice",
285 "SELECT * FROM issues WHERE assignee = 'alice'",
286 "ASSIGN issue backend#1 TO alice",
287 "COMMENT ON ISSUE backend#1 WITH 'Working on it'",
288 "UPDATE issue backend#1 SET status = 'in-progress'",
289 "CLOSE issue backend#1 WITH done",
290 ];
291
292 for query in queries {
293 let result = parse_query(query).unwrap();
294 insta::assert_debug_snapshot!(&result);
295 }
296 }
297
298 #[test]
299 fn test_string_with_multiple_escapes() {
300 let query =
301 r"CREATE ISSUE OF KIND bug IN test WITH TITLE 'Line1\nLine2\tTab\rReturn\\Backslash'";
302 let result = parse_query(query).unwrap();
303 insta::assert_debug_snapshot!(&result);
304 }
305
306 #[test]
307 fn test_negative_numbers() {
308 let query = "UPDATE issue test#100 SET count = -50";
309 let result = parse_query(query).unwrap();
310 insta::assert_debug_snapshot!(&result);
311 }
312
313 #[test]
314 fn test_float_values() {
315 let query = "UPDATE issue test#100 SET score = 3.14159";
316 let result = parse_query(query).unwrap();
317 insta::assert_debug_snapshot!(&result);
318 }
319
320 #[test]
321 fn test_deeply_nested_filters() {
322 let query = "SELECT * FROM issues WHERE ((a = 1 AND b = 2) OR (c = 3 AND d = 4)) AND e = 5";
323 let result = parse_query(query).unwrap();
324 insta::assert_debug_snapshot!(&result);
325 }
326
327 #[test]
328 fn test_not_with_parentheses() {
329 let query = "SELECT * FROM issues WHERE NOT (status = 'closed' OR status = 'archived')";
330 let result = parse_query(query).unwrap();
331 insta::assert_debug_snapshot!(&result);
332 }
333
334 #[test]
335 fn test_in_with_priorities() {
336 let query = "SELECT * FROM issues WHERE priority IN (critical, high, medium)";
337 let result = parse_query(query).unwrap();
338 insta::assert_debug_snapshot!(&result);
339 }
340
341 #[test]
342 fn test_in_with_strings() {
343 let query = "SELECT * FROM issues WHERE status IN ('open', 'in-progress', 'review')";
344 let result = parse_query(query).unwrap();
345 insta::assert_debug_snapshot!(&result);
346 }
347
348 #[test]
349 fn test_comparison_operators() {
350 let queries = vec![
351 "SELECT * FROM issues WHERE count > 10",
352 "SELECT * FROM issues WHERE count < 5",
353 "SELECT * FROM issues WHERE count >= 10",
354 "SELECT * FROM issues WHERE count <= 5",
355 "SELECT * FROM issues WHERE status != 'closed'",
356 ];
357 for query in queries {
358 let result = parse_query(query).unwrap();
359 insta::assert_debug_snapshot!(&result);
360 }
361 }
362
363 #[test]
364 fn test_case_insensitive_keywords() {
365 let queries = vec![
366 "select * from issues",
367 "SELECT * FROM ISSUES",
368 "SeLeCt * FrOm IsSuEs",
369 "create user alice",
370 "CREATE USER ALICE",
371 ];
372 for query in queries {
373 let result = parse_query(query).unwrap();
374 insta::assert_debug_snapshot!(&result);
375 }
376 }
377
378 #[test]
379 fn test_hyphenated_identifiers() {
380 let queries = vec![
381 "CREATE USER my-user-name",
382 "CREATE PROJECT my-cool-project",
383 "SELECT * FROM issues WHERE project = 'my-backend-api'",
384 ];
385 for query in queries {
386 let result = parse_query(query).unwrap();
387 insta::assert_debug_snapshot!(&result);
388 }
389 }
390
391 #[test]
392 fn test_keywords_as_field_names() {
393 let queries = vec![
394 "SELECT project, user, issue FROM issues",
395 "SELECT * FROM issues WHERE project = 'test'",
396 "SELECT * FROM issues WHERE user = 'alice'",
397 "UPDATE issue test#1 SET comment = 'test'",
398 ];
399 for query in queries {
400 let result = parse_query(query).unwrap();
401 insta::assert_debug_snapshot!(&result);
402 }
403 }
404
405 #[test]
406 fn test_all_field_keywords_in_create() {
407 let query = "CREATE ISSUE OF KIND bug IN test WITH TITLE 'T' DESCRIPTION 'D' PRIORITY high ASSIGNEE alice";
408 let result = parse_query(query).unwrap();
409 insta::assert_debug_snapshot!(&result);
410 }
411
412 #[test]
413 fn test_all_delete_targets() {
414 let queries = vec![
415 "DELETE user alice",
416 "DELETE project backend",
417 "DELETE issue backend#456",
418 "DELETE comment 789",
419 ];
420 for query in queries {
421 let result = parse_query(query).unwrap();
422 insta::assert_debug_snapshot!(&result);
423 }
424 }
425
426 #[test]
427 fn test_all_update_targets() {
428 let queries = vec![
429 "UPDATE user alice SET email = 'new@test.com'",
430 "UPDATE project backend SET name = 'New Name'",
431 "UPDATE issue backend#123 SET status = 'closed'",
432 "UPDATE issue backend#456 SET priority = high",
433 "UPDATE comment C789 SET content = 'updated'",
434 ];
435 for query in queries {
436 let result = parse_query(query).unwrap();
437 insta::assert_debug_snapshot!(&result);
438 }
439 }
440
441 #[test]
442 fn test_multiple_columns_select() {
443 let query =
444 "SELECT id, title, status, priority, assignee, created_at, updated_at FROM issues";
445 let result = parse_query(query).unwrap();
446 insta::assert_debug_snapshot!(&result);
447 }
448
449 #[test]
450 fn test_limit_and_offset_together() {
451 let query = "SELECT * FROM issues LIMIT 50 OFFSET 100";
452 let result = parse_query(query).unwrap();
453 insta::assert_debug_snapshot!(&result);
454 }
455
456 #[test]
457 fn test_order_by_asc_explicit() {
458 let query = "SELECT * FROM issues ORDER BY created_at ASC";
459 let result = parse_query(query).unwrap();
460 insta::assert_debug_snapshot!(&result);
461 }
462
463 #[test]
464 fn test_boolean_values() {
465 let queries = vec![
466 "UPDATE issue backend#1 SET active = true",
467 "UPDATE issue backend#1 SET archived = false",
468 "SELECT * FROM issues WHERE active = TRUE",
469 "SELECT * FROM issues WHERE archived = FALSE",
470 ];
471 for query in queries {
472 let result = parse_query(query).unwrap();
473 insta::assert_debug_snapshot!(&result);
474 }
475 }
476
477 #[test]
478 fn test_null_values() {
479 let queries = vec![
480 "UPDATE issue backend#1 SET assignee = null",
481 "SELECT * FROM issues WHERE assignee = NULL",
482 ];
483 for query in queries {
484 let result = parse_query(query).unwrap();
485 insta::assert_debug_snapshot!(&result);
486 }
487 }
488
489 #[test]
490 fn test_comment_statement() {
491 let query = "COMMENT ON ISSUE backend#123 WITH 'Quick comment'";
492 let result = parse_query(query).unwrap();
493 insta::assert_debug_snapshot!(&result);
494 }
495
496 #[test]
497 fn test_close_with_and_without_reason() {
498 let queries = vec![
499 "CLOSE issue backend#123",
500 "CLOSE issue backend#123 WITH done",
501 "CLOSE issue backend#456 WITH duplicate",
502 ];
503 for query in queries {
504 let result = parse_query(query).unwrap();
505 insta::assert_debug_snapshot!(&result);
506 }
507 }
508
509 #[test]
510 fn test_empty_string_value() {
511 let query = "UPDATE issue backend#1 SET description = ''";
512 let result = parse_query(query).unwrap();
513 insta::assert_debug_snapshot!(&result);
514 }
515
516 #[test]
517 fn test_special_characters_in_strings() {
518 let query = r"CREATE ISSUE OF KIND bug IN test WITH TITLE 'Special chars: !@#$%^&*()_+-={}[]|:;<>?,./~`'";
519 let result = parse_query(query).unwrap();
520 insta::assert_debug_snapshot!(&result);
521 }
522
523 #[test]
524 fn test_double_quotes_in_strings() {
525 let query = r#"CREATE ISSUE OF KIND bug IN test WITH TITLE "Double quoted string""#;
526 let result = parse_query(query).unwrap();
527 insta::assert_debug_snapshot!(&result);
528 }
529
530 #[test]
531 fn test_complex_real_world_query() {
532 let query = r#"
533 SELECT title, status, priority, assignee, created_at
534 FROM issues
535 WHERE (priority = critical OR priority = high)
536 AND status IN ('open', 'in-progress')
537 AND assignee IS NOT NULL
538 AND project = 'backend'
539 ORDER BY priority DESC
540 LIMIT 25
541 OFFSET 0
542 "#;
543 let result = parse_query(query).unwrap();
544 insta::assert_debug_snapshot!(&result);
545 }
546
547 #[test]
548 fn test_minimal_create_user() {
549 let query = "CREATE USER alice";
550 let result = parse_query(query).unwrap();
551 insta::assert_debug_snapshot!(&result);
552 }
553
554 #[test]
555 fn test_minimal_create_project() {
556 let query = "CREATE PROJECT test";
557 let result = parse_query(query).unwrap();
558 insta::assert_debug_snapshot!(&result);
559 }
560
561 #[test]
562 fn test_select_from_all_entities() {
563 for entity in &["users", "projects", "issues", "comments"] {
564 let query = format!("SELECT * FROM {}", entity);
565 let result = parse_query(&query).unwrap();
566 insta::assert_debug_snapshot!(&result);
567 }
568 }
569
570 #[test]
571 fn test_issue_id_variations() {
572 let queries = vec![
573 "CLOSE issue a#1",
574 "CLOSE issue my-project#123",
575 "CLOSE issue backend_api#456",
576 ];
577 for query in queries {
578 let result = parse_query(query).unwrap();
579 insta::assert_debug_snapshot!(&result);
580 }
581 }
582
583 #[test]
584 fn test_priority_in_different_cases() {
585 let queries = vec![
586 "CREATE ISSUE OF KIND bug IN test WITH TITLE 'T' PRIORITY critical",
587 "CREATE ISSUE OF KIND bug IN test WITH TITLE 'T' PRIORITY CRITICAL",
588 "CREATE ISSUE OF KIND bug IN test WITH TITLE 'T' PRIORITY Critical",
589 ];
590 for query in queries {
591 let result = parse_query(query).unwrap();
592 insta::assert_debug_snapshot!(&result);
593 }
594 }
595
596 #[test]
597 fn test_all_comparison_ops_with_strings() {
598 let query = "SELECT * FROM issues WHERE title LIKE '%bug%'";
599 let result = parse_query(query).unwrap();
600 insta::assert_debug_snapshot!(&result);
601 }
602
603 #[test]
604 fn test_single_column_select() {
605 let query = "SELECT title FROM issues";
606 let result = parse_query(query).unwrap();
607 insta::assert_debug_snapshot!(&result);
608 }
609
610 #[test]
611 fn test_whitespace_variations() {
612 let queries = vec![
613 "SELECT * FROM issues",
614 "SELECT * FROM issues",
615 "SELECT\t*\tFROM\tissues",
616 "SELECT\n*\nFROM\nissues",
617 ];
618 for query in queries {
619 let result = parse_query(query).unwrap();
620 insta::assert_debug_snapshot!(&result);
621 }
622 }
623
624 #[test]
625 fn test_field_update_with_priority() {
626 let query = "UPDATE issue backend#1 SET priority = critical, status = 'open'";
627 let result = parse_query(query).unwrap();
628 insta::assert_debug_snapshot!(&result);
629 }
630}