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