issuecraft_ql/
lib.rs

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<Statement> {
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 project with the name '{0}' exists")]
29    ProjectNotFound(String),
30    #[error("No issue with the name '{0}' exists")]
31    IssueNotFound(String),
32    #[error("The issue withe the name '{0}' was already closed. Reason '{1}'")]
33    IssueAlreadyClosed(String, CloseReason),
34    #[error("{0}")]
35    ImplementationSpecific(String),
36}
37
38#[async_trait]
39pub trait ExecutionEngine {
40    async fn execute(&mut self, query: &str) -> 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);
113        assert!(result.is_ok());
114
115        if let Ok(Statement::Create(CreateStatement::User {
116            username,
117            email,
118            name,
119        })) = result
120        {
121            assert_eq!(username, "john_doe");
122            assert_eq!(email, Some("john@example.com".to_string()));
123            assert_eq!(name, Some("John Doe".to_string()));
124        } else {
125            panic!("Expected CreateStatement::User");
126        }
127    }
128
129    #[test]
130    fn test_parse_create_project() {
131        let query = "CREATE PROJECT my-project WITH NAME 'My Project' DESCRIPTION 'A test project'";
132        let result = parse_query(query);
133        assert!(result.is_ok());
134    }
135
136    #[test]
137    fn test_parse_create_issue() {
138        let query = "CREATE ISSUE IN my-project WITH TITLE 'Bug found' DESCRIPTION 'Something broke' PRIORITY high ASSIGNEE john_doe";
139        let result = parse_query(query);
140        assert!(result.is_ok());
141    }
142
143    #[test]
144    fn test_parse_select_all() {
145        let query = "SELECT * FROM issues";
146        let result = parse_query(query);
147        assert!(result.is_ok());
148    }
149
150    #[test]
151    fn test_parse_select_with_where() {
152        let query = "SELECT * FROM issues WHERE status = 'open' AND priority = high";
153        let result = parse_query(query);
154        assert!(result.is_ok());
155    }
156
157    #[test]
158    fn test_parse_update() {
159        let query = "UPDATE issue my-project#123 SET status = 'closed', priority = low";
160        let result = parse_query(query);
161        assert!(result.is_ok());
162    }
163
164    #[test]
165    fn test_parse_delete() {
166        let query = "DELETE issue my-project#456";
167        let result = parse_query(query);
168        assert!(result.is_ok());
169    }
170
171    #[test]
172    fn test_parse_assign() {
173        let query = "ASSIGN issue my-project#789 TO alice";
174        let result = parse_query(query);
175        assert!(result.is_ok());
176    }
177
178    #[test]
179    fn test_parse_close() {
180        let query = "CLOSE issue my-project#101";
181        let result = parse_query(query);
182        assert!(result.is_ok());
183    }
184
185    #[test]
186    fn test_parse_comment() {
187        let query = "COMMENT ON issue my-project#202 WITH 'This is a comment'";
188        let result = parse_query(query);
189        assert!(result.is_ok());
190    }
191
192    #[test]
193    fn test_parse_complex_query() {
194        let query = "SELECT title, status, assignee FROM issues WHERE project = 'backend' AND (priority = high OR status = 'critical') ORDER BY created_at DESC LIMIT 10";
195        let result = parse_query(query);
196        if let Err(ref e) = result {
197            eprintln!("Parse error: {}", e);
198        }
199        assert!(result.is_ok());
200    }
201
202    #[test]
203    fn test_parse_project_qualified_issue() {
204        let query = "CLOSE issue my-project#42 WITH 'Completed'";
205        let result = parse_query(query);
206        assert!(result.is_ok());
207    }
208
209    #[test]
210    fn test_parse_labels() {
211        let query = "CREATE ISSUE IN frontend WITH TITLE 'Test' LABELS [bug, urgent, frontend]";
212        let result = parse_query(query);
213        assert!(result.is_ok());
214    }
215
216    #[test]
217    fn test_parse_multiple_field_updates() {
218        let query = "UPDATE issue my-project#100 SET status = 'closed', priority = medium, assignee = 'bob'";
219        let result = parse_query(query);
220        assert!(result.is_ok());
221    }
222
223    #[test]
224    fn test_parse_in_operator() {
225        let query = "SELECT * FROM issues WHERE priority IN (critical, high)";
226        let result = parse_query(query);
227        assert!(result.is_ok());
228    }
229
230    #[test]
231    fn test_parse_is_null() {
232        let query = "SELECT * FROM issues WHERE assignee IS NULL";
233        let result = parse_query(query);
234        assert!(result.is_ok());
235    }
236
237    #[test]
238    fn test_parse_is_not_null() {
239        let query = "SELECT * FROM issues WHERE assignee IS NOT NULL";
240        let result = parse_query(query);
241        assert!(result.is_ok());
242    }
243
244    #[test]
245    fn test_parse_not_operator() {
246        let query = "SELECT * FROM issues WHERE NOT status = 'closed'";
247        let result = parse_query(query);
248        assert!(result.is_ok());
249    }
250
251    #[test]
252    fn test_parse_like_operator() {
253        let query = "SELECT * FROM issues WHERE title LIKE '%bug%'";
254        let result = parse_query(query);
255        assert!(result.is_ok());
256    }
257
258    #[test]
259    fn test_parse_order_asc() {
260        let query = "SELECT * FROM issues ORDER BY created_at ASC";
261        let result = parse_query(query);
262        assert!(result.is_ok());
263    }
264
265    #[test]
266    fn test_parse_offset() {
267        let query = "SELECT * FROM issues LIMIT 10 OFFSET 20";
268        let result = parse_query(query);
269        assert!(result.is_ok());
270    }
271
272    #[test]
273    fn test_parse_all_priorities() {
274        let queries = vec![
275            "CREATE ISSUE IN test WITH TITLE 'Test' PRIORITY critical",
276            "CREATE ISSUE IN test WITH TITLE 'Test' PRIORITY high",
277            "CREATE ISSUE IN test WITH TITLE 'Test' PRIORITY medium",
278            "CREATE ISSUE IN test WITH TITLE 'Test' PRIORITY low",
279        ];
280
281        for query in queries {
282            let result = parse_query(query);
283            assert!(result.is_ok(), "Failed to parse: {}", query);
284        }
285    }
286
287    #[test]
288    fn test_parse_all_entity_types() {
289        let queries = vec![
290            "SELECT * FROM users",
291            "SELECT * FROM projects",
292            "SELECT * FROM issues",
293            "SELECT * FROM comments",
294        ];
295
296        for query in queries {
297            let result = parse_query(query);
298            assert!(result.is_ok(), "Failed to parse: {}", query);
299        }
300    }
301
302    #[test]
303    fn test_integration_workflow() {
304        let queries = vec![
305            "CREATE USER alice WITH EMAIL 'alice@test.com' NAME 'Alice'",
306            "CREATE PROJECT backend WITH NAME 'Backend' OWNER alice",
307            "CREATE ISSUE IN backend WITH TITLE 'Bug fix' PRIORITY high ASSIGNEE alice",
308            "SELECT * FROM issues WHERE assignee = 'alice'",
309            "ASSIGN issue backend#1 TO alice",
310            "COMMENT ON ISSUE backend#1 WITH 'Working on it'",
311            "UPDATE issue backend#1 SET status = 'in-progress'",
312            "CLOSE issue backend#1 WITH 'Fixed'",
313        ];
314
315        for query in queries {
316            let result = parse_query(query);
317            assert!(result.is_ok(), "Failed to parse: {}", query);
318        }
319    }
320
321    #[test]
322    fn test_empty_labels() {
323        let query = "CREATE ISSUE IN test WITH TITLE 'Test' LABELS []";
324        let result = parse_query(query);
325        assert!(result.is_ok());
326        if let Ok(Statement::Create(CreateStatement::Issue { labels, .. })) = result {
327            assert_eq!(labels.len(), 0);
328        }
329    }
330
331    #[test]
332    fn test_string_with_multiple_escapes() {
333        let query = r"CREATE ISSUE IN test WITH TITLE 'Line1\nLine2\tTab\rReturn\\Backslash'";
334        let result = parse_query(query);
335        assert!(result.is_ok());
336    }
337
338    #[test]
339    fn test_negative_numbers() {
340        let query = "UPDATE issue test#100 SET count = -50";
341        let result = parse_query(query);
342        assert!(result.is_ok());
343    }
344
345    #[test]
346    fn test_float_values() {
347        let query = "UPDATE issue test#100 SET score = 3.14159";
348        let result = parse_query(query);
349        assert!(result.is_ok());
350    }
351
352    #[test]
353    fn test_deeply_nested_filters() {
354        let query = "SELECT * FROM issues WHERE ((a = 1 AND b = 2) OR (c = 3 AND d = 4)) AND e = 5";
355        let result = parse_query(query);
356        assert!(result.is_ok());
357    }
358
359    #[test]
360    fn test_not_with_parentheses() {
361        let query = "SELECT * FROM issues WHERE NOT (status = 'closed' OR status = 'archived')";
362        let result = parse_query(query);
363        assert!(result.is_ok());
364    }
365
366    #[test]
367    fn test_in_with_priorities() {
368        let query = "SELECT * FROM issues WHERE priority IN (critical, high, medium)";
369        let result = parse_query(query);
370        assert!(result.is_ok());
371    }
372
373    #[test]
374    fn test_in_with_strings() {
375        let query = "SELECT * FROM issues WHERE status IN ('open', 'in-progress', 'review')";
376        let result = parse_query(query);
377        assert!(result.is_ok());
378    }
379
380    #[test]
381    fn test_comparison_operators() {
382        let queries = vec![
383            "SELECT * FROM issues WHERE count > 10",
384            "SELECT * FROM issues WHERE count < 5",
385            "SELECT * FROM issues WHERE count >= 10",
386            "SELECT * FROM issues WHERE count <= 5",
387            "SELECT * FROM issues WHERE status != 'closed'",
388        ];
389        for query in queries {
390            let result = parse_query(query);
391            assert!(result.is_ok(), "Failed: {}", query);
392        }
393    }
394
395    #[test]
396    fn test_case_insensitive_keywords() {
397        let queries = vec![
398            "select * from issues",
399            "SELECT * FROM ISSUES",
400            "SeLeCt * FrOm IsSuEs",
401            "create user alice",
402            "CREATE USER ALICE",
403        ];
404        for query in queries {
405            let result = parse_query(query);
406            assert!(result.is_ok(), "Failed: {}", query);
407        }
408    }
409
410    #[test]
411    fn test_hyphenated_identifiers() {
412        let queries = vec![
413            "CREATE USER my-user-name",
414            "CREATE PROJECT my-cool-project",
415            "SELECT * FROM issues WHERE project = 'my-backend-api'",
416        ];
417        for query in queries {
418            let result = parse_query(query);
419            assert!(result.is_ok(), "Failed: {}", query);
420        }
421    }
422
423    #[test]
424    fn test_keywords_as_field_names() {
425        let queries = vec![
426            "SELECT project, user, issue FROM issues",
427            "SELECT * FROM issues WHERE project = 'test'",
428            "SELECT * FROM issues WHERE user = 'alice'",
429            "UPDATE issue test#1 SET comment = 'test'",
430        ];
431        for query in queries {
432            let result = parse_query(query);
433            assert!(result.is_ok(), "Failed: {}", query);
434        }
435    }
436
437    #[test]
438    fn test_all_field_keywords_in_create() {
439        let query = "CREATE ISSUE IN test WITH TITLE 'T' DESCRIPTION 'D' PRIORITY high ASSIGNEE alice LABELS [bug]";
440        let result = parse_query(query);
441        assert!(result.is_ok());
442    }
443
444    #[test]
445    fn test_all_delete_targets() {
446        let queries = vec![
447            "DELETE user alice",
448            "DELETE project backend",
449            "DELETE issue backend#456",
450            "DELETE comment 789",
451        ];
452        for query in queries {
453            let result = parse_query(query);
454            assert!(result.is_ok(), "Failed: {}", query);
455        }
456    }
457
458    #[test]
459    fn test_all_update_targets() {
460        let queries = vec![
461            "UPDATE user alice SET email = 'new@test.com'",
462            "UPDATE project backend SET name = 'New Name'",
463            "UPDATE issue backend#123 SET status = 'closed'",
464            "UPDATE issue backend#456 SET priority = high",
465            "UPDATE comment 789 SET content = 'updated'",
466        ];
467        for query in queries {
468            let result = parse_query(query);
469            assert!(result.is_ok(), "Failed: {}", query);
470        }
471    }
472
473    #[test]
474    fn test_multiple_columns_select() {
475        let query =
476            "SELECT id, title, status, priority, assignee, created_at, updated_at FROM issues";
477        let result = parse_query(query);
478        assert!(result.is_ok());
479        if let Ok(Statement::Select(select)) = result {
480            assert_eq!(select.columns.len(), 7);
481        }
482    }
483
484    #[test]
485    fn test_limit_and_offset_together() {
486        let query = "SELECT * FROM issues LIMIT 50 OFFSET 100";
487        let result = parse_query(query);
488        assert!(result.is_ok());
489        if let Ok(Statement::Select(select)) = result {
490            assert_eq!(select.limit, Some(50));
491            assert_eq!(select.offset, Some(100));
492        }
493    }
494
495    #[test]
496    fn test_order_by_asc_explicit() {
497        let query = "SELECT * FROM issues ORDER BY created_at ASC";
498        let result = parse_query(query);
499        assert!(result.is_ok());
500        if let Ok(Statement::Select(select)) = result {
501            assert!(select.order_by.is_some());
502            let order = select.order_by.unwrap();
503            assert_eq!(order.direction, OrderDirection::Asc);
504        }
505    }
506
507    #[test]
508    fn test_boolean_values() {
509        let queries = vec![
510            "UPDATE issue backend#1 SET active = true",
511            "UPDATE issue backend#1 SET archived = false",
512            "SELECT * FROM issues WHERE active = TRUE",
513            "SELECT * FROM issues WHERE archived = FALSE",
514        ];
515        for query in queries {
516            let result = parse_query(query);
517            assert!(result.is_ok(), "Failed: {}", query);
518        }
519    }
520
521    #[test]
522    fn test_null_values() {
523        let queries = vec![
524            "UPDATE issue backend#1 SET assignee = null",
525            "SELECT * FROM issues WHERE assignee = NULL",
526        ];
527        for query in queries {
528            let result = parse_query(query);
529            assert!(result.is_ok(), "Failed: {}", query);
530        }
531    }
532
533    #[test]
534    fn test_create_comment_variations() {
535        let queries = vec![
536            "CREATE COMMENT ON ISSUE backend#123 WITH 'Simple comment'",
537            "CREATE COMMENT ON ISSUE backend#123 WITH 'Comment' AUTHOR alice",
538            "CREATE COMMENT ON ISSUE backend#456 WITH 'Project issue comment'",
539        ];
540        for query in queries {
541            let result = parse_query(query);
542            assert!(result.is_ok(), "Failed: {}", query);
543        }
544    }
545
546    #[test]
547    fn test_comment_statement() {
548        let query = "COMMENT ON ISSUE backend#123 WITH 'Quick comment'";
549        let result = parse_query(query);
550        assert!(result.is_ok());
551    }
552
553    #[test]
554    fn test_close_with_and_without_reason() {
555        let queries = vec![
556            "CLOSE issue backend#123",
557            "CLOSE issue backend#123 WITH 'Completed'",
558            "CLOSE issue backend#456 WITH 'Duplicate of #455'",
559        ];
560        for query in queries {
561            let result = parse_query(query);
562            assert!(result.is_ok(), "Failed: {}", query);
563        }
564    }
565
566    #[test]
567    fn test_empty_string_value() {
568        let query = "UPDATE issue backend#1 SET description = ''";
569        let result = parse_query(query);
570        assert!(result.is_ok());
571    }
572
573    #[test]
574    fn test_special_characters_in_strings() {
575        let query =
576            r"CREATE ISSUE IN test WITH TITLE 'Special chars: !@#$%^&*()_+-={}[]|:;<>?,./~`'";
577        let result = parse_query(query);
578        assert!(result.is_ok());
579    }
580
581    #[test]
582    fn test_double_quotes_in_strings() {
583        let query = r#"CREATE ISSUE IN test WITH TITLE "Double quoted string""#;
584        let result = parse_query(query);
585        assert!(result.is_ok());
586    }
587
588    #[test]
589    fn test_labels_with_hyphens() {
590        let query =
591            "CREATE ISSUE IN test WITH TITLE 'Test' LABELS [high-priority, bug-fix, ui-component]";
592        let result = parse_query(query);
593        assert!(result.is_ok());
594    }
595
596    #[test]
597    fn test_complex_real_world_query() {
598        let query = r#"
599            SELECT title, status, priority, assignee, created_at
600            FROM issues
601            WHERE (priority = critical OR priority = high)
602              AND status IN ('open', 'in-progress')
603              AND assignee IS NOT NULL
604              AND project = 'backend'
605            ORDER BY priority DESC
606            LIMIT 25
607            OFFSET 0
608        "#;
609        let result = parse_query(query);
610        assert!(result.is_ok());
611    }
612
613    #[test]
614    fn test_minimal_create_user() {
615        let query = "CREATE USER alice";
616        let result = parse_query(query);
617        assert!(result.is_ok());
618        if let Ok(Statement::Create(CreateStatement::User { email, name, .. })) = result {
619            assert!(email.is_none());
620            assert!(name.is_none());
621        }
622    }
623
624    #[test]
625    fn test_minimal_create_project() {
626        let query = "CREATE PROJECT test";
627        let result = parse_query(query);
628        assert!(result.is_ok());
629    }
630
631    #[test]
632    fn test_select_from_all_entities() {
633        for entity in &["users", "projects", "issues", "comments"] {
634            let query = format!("SELECT * FROM {}", entity);
635            let result = parse_query(&query);
636            assert!(result.is_ok(), "Failed: {}", query);
637        }
638    }
639
640    #[test]
641    fn test_issue_id_variations() {
642        let queries = vec![
643            "CLOSE issue a#1",
644            "CLOSE issue my-project#123",
645            "CLOSE issue backend_api#456",
646        ];
647        for query in queries {
648            let result = parse_query(query);
649            assert!(result.is_ok(), "Failed: {}", query);
650        }
651    }
652
653    #[test]
654    fn test_priority_in_different_cases() {
655        let queries = vec![
656            "CREATE ISSUE IN test WITH TITLE 'T' PRIORITY critical",
657            "CREATE ISSUE IN test WITH TITLE 'T' PRIORITY CRITICAL",
658            "CREATE ISSUE IN test WITH TITLE 'T' PRIORITY Critical",
659        ];
660        for query in queries {
661            let result = parse_query(query);
662            assert!(result.is_ok(), "Failed: {}", query);
663        }
664    }
665
666    #[test]
667    fn test_all_comparison_ops_with_strings() {
668        let query = "SELECT * FROM issues WHERE title LIKE '%bug%'";
669        let result = parse_query(query);
670        assert!(result.is_ok());
671    }
672
673    #[test]
674    fn test_single_column_select() {
675        let query = "SELECT title FROM issues";
676        let result = parse_query(query);
677        assert!(result.is_ok());
678        if let Ok(Statement::Select(select)) = result {
679            assert_eq!(select.columns.len(), 1);
680        }
681    }
682
683    #[test]
684    fn test_whitespace_variations() {
685        let queries = vec![
686            "SELECT * FROM issues",
687            "SELECT  *  FROM  issues",
688            "SELECT\t*\tFROM\tissues",
689            "SELECT\n*\nFROM\nissues",
690        ];
691        for query in queries {
692            let result = parse_query(query);
693            assert!(result.is_ok(), "Failed: {}", query);
694        }
695    }
696
697    #[test]
698    fn test_field_update_with_priority() {
699        let query = "UPDATE issue backend#1 SET priority = critical, status = 'open'";
700        let result = parse_query(query);
701        assert!(result.is_ok());
702    }
703
704    #[test]
705    fn test_field_update_with_identifier() {
706        let query = "UPDATE issue backend#1 SET assignee = alice, project = backend";
707        let result = parse_query(query);
708        assert!(result.is_ok());
709    }
710}