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 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: &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 done";
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 done",
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_string_with_multiple_escapes() {
323        let query = r"CREATE ISSUE IN test WITH TITLE 'Line1\nLine2\tTab\rReturn\\Backslash'";
324        let result = parse_query(query);
325        assert!(result.is_ok());
326    }
327
328    #[test]
329    fn test_negative_numbers() {
330        let query = "UPDATE issue test#100 SET count = -50";
331        let result = parse_query(query);
332        assert!(result.is_ok());
333    }
334
335    #[test]
336    fn test_float_values() {
337        let query = "UPDATE issue test#100 SET score = 3.14159";
338        let result = parse_query(query);
339        assert!(result.is_ok());
340    }
341
342    #[test]
343    fn test_deeply_nested_filters() {
344        let query = "SELECT * FROM issues WHERE ((a = 1 AND b = 2) OR (c = 3 AND d = 4)) AND e = 5";
345        let result = parse_query(query);
346        assert!(result.is_ok());
347    }
348
349    #[test]
350    fn test_not_with_parentheses() {
351        let query = "SELECT * FROM issues WHERE NOT (status = 'closed' OR status = 'archived')";
352        let result = parse_query(query);
353        assert!(result.is_ok());
354    }
355
356    #[test]
357    fn test_in_with_priorities() {
358        let query = "SELECT * FROM issues WHERE priority IN (critical, high, medium)";
359        let result = parse_query(query);
360        assert!(result.is_ok());
361    }
362
363    #[test]
364    fn test_in_with_strings() {
365        let query = "SELECT * FROM issues WHERE status IN ('open', 'in-progress', 'review')";
366        let result = parse_query(query);
367        assert!(result.is_ok());
368    }
369
370    #[test]
371    fn test_comparison_operators() {
372        let queries = vec![
373            "SELECT * FROM issues WHERE count > 10",
374            "SELECT * FROM issues WHERE count < 5",
375            "SELECT * FROM issues WHERE count >= 10",
376            "SELECT * FROM issues WHERE count <= 5",
377            "SELECT * FROM issues WHERE status != 'closed'",
378        ];
379        for query in queries {
380            let result = parse_query(query);
381            assert!(
382                result.is_ok(),
383                "Failed query '{}' with error {}",
384                query,
385                result.err().unwrap()
386            );
387        }
388    }
389
390    #[test]
391    fn test_case_insensitive_keywords() {
392        let queries = vec![
393            "select * from issues",
394            "SELECT * FROM ISSUES",
395            "SeLeCt * FrOm IsSuEs",
396            "create user alice",
397            "CREATE USER ALICE",
398        ];
399        for query in queries {
400            let result = parse_query(query);
401            assert!(
402                result.is_ok(),
403                "Failed query '{}' with error {}",
404                query,
405                result.err().unwrap()
406            );
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!(
420                result.is_ok(),
421                "Failed query '{}' with error {}",
422                query,
423                result.err().unwrap()
424            );
425        }
426    }
427
428    #[test]
429    fn test_keywords_as_field_names() {
430        let queries = vec![
431            "SELECT project, user, issue FROM issues",
432            "SELECT * FROM issues WHERE project = 'test'",
433            "SELECT * FROM issues WHERE user = 'alice'",
434            "UPDATE issue test#1 SET comment = 'test'",
435        ];
436        for query in queries {
437            let result = parse_query(query);
438            assert!(
439                result.is_ok(),
440                "Failed query '{}' with error {}",
441                query,
442                result.err().unwrap()
443            );
444        }
445    }
446
447    #[test]
448    fn test_all_field_keywords_in_create() {
449        let query = "CREATE ISSUE IN test WITH TITLE 'T' DESCRIPTION 'D' PRIORITY high ASSIGNEE alice LABELS [bug]";
450        let result = parse_query(query);
451        assert!(result.is_ok());
452    }
453
454    #[test]
455    fn test_all_delete_targets() {
456        let queries = vec![
457            "DELETE user alice",
458            "DELETE project backend",
459            "DELETE issue backend#456",
460            "DELETE comment 789",
461        ];
462        for query in queries {
463            let result = parse_query(query);
464            assert!(
465                result.is_ok(),
466                "Failed query '{}' with error {}",
467                query,
468                result.err().unwrap()
469            );
470        }
471    }
472
473    #[test]
474    fn test_all_update_targets() {
475        let queries = vec![
476            "UPDATE user alice SET email = 'new@test.com'",
477            "UPDATE project backend SET name = 'New Name'",
478            "UPDATE issue backend#123 SET status = 'closed'",
479            "UPDATE issue backend#456 SET priority = high",
480            "UPDATE comment C789 SET content = 'updated'",
481        ];
482        for query in queries {
483            let result = parse_query(query);
484            assert!(
485                result.is_ok(),
486                "Failed query '{}' with error {}",
487                query,
488                result.err().unwrap()
489            );
490        }
491    }
492
493    #[test]
494    fn test_multiple_columns_select() {
495        let query =
496            "SELECT id, title, status, priority, assignee, created_at, updated_at FROM issues";
497        let result = parse_query(query);
498        assert!(result.is_ok());
499        if let Ok(Statement::Select(select)) = result {
500            assert_eq!(select.columns.count(), 7);
501        }
502    }
503
504    #[test]
505    fn test_limit_and_offset_together() {
506        let query = "SELECT * FROM issues LIMIT 50 OFFSET 100";
507        let result = parse_query(query);
508        assert!(result.is_ok());
509        if let Ok(Statement::Select(select)) = result {
510            assert_eq!(select.limit, Some(50));
511            assert_eq!(select.offset, Some(100));
512        }
513    }
514
515    #[test]
516    fn test_order_by_asc_explicit() {
517        let query = "SELECT * FROM issues ORDER BY created_at ASC";
518        let result = parse_query(query);
519        assert!(result.is_ok());
520        if let Ok(Statement::Select(select)) = result {
521            assert!(select.order_by.is_some());
522            let order = select.order_by.unwrap();
523            assert_eq!(order.direction, OrderDirection::Asc);
524        }
525    }
526
527    #[test]
528    fn test_boolean_values() {
529        let queries = vec![
530            "UPDATE issue backend#1 SET active = true",
531            "UPDATE issue backend#1 SET archived = false",
532            "SELECT * FROM issues WHERE active = TRUE",
533            "SELECT * FROM issues WHERE archived = FALSE",
534        ];
535        for query in queries {
536            let result = parse_query(query);
537            assert!(
538                result.is_ok(),
539                "Failed query '{}' with error {}",
540                query,
541                result.err().unwrap()
542            );
543        }
544    }
545
546    #[test]
547    fn test_null_values() {
548        let queries = vec![
549            "UPDATE issue backend#1 SET assignee = null",
550            "SELECT * FROM issues WHERE assignee = NULL",
551        ];
552        for query in queries {
553            let result = parse_query(query);
554            assert!(
555                result.is_ok(),
556                "Failed query '{}' with error {}",
557                query,
558                result.err().unwrap()
559            );
560        }
561    }
562
563    #[test]
564    fn test_comment_statement() {
565        let query = "COMMENT ON ISSUE backend#123 WITH 'Quick comment'";
566        let result = parse_query(query);
567        assert!(result.is_ok());
568    }
569
570    #[test]
571    fn test_close_with_and_without_reason() {
572        let queries = vec![
573            "CLOSE issue backend#123",
574            "CLOSE issue backend#123 WITH done",
575            "CLOSE issue backend#456 WITH duplicate",
576        ];
577        for query in queries {
578            let result = parse_query(query);
579            assert!(
580                result.is_ok(),
581                "Failed query '{}' with error {}",
582                query,
583                result.err().unwrap()
584            );
585        }
586    }
587
588    #[test]
589    fn test_empty_string_value() {
590        let query = "UPDATE issue backend#1 SET description = ''";
591        let result = parse_query(query);
592        assert!(result.is_ok());
593    }
594
595    #[test]
596    fn test_special_characters_in_strings() {
597        let query =
598            r"CREATE ISSUE IN test WITH TITLE 'Special chars: !@#$%^&*()_+-={}[]|:;<>?,./~`'";
599        let result = parse_query(query);
600        assert!(result.is_ok());
601    }
602
603    #[test]
604    fn test_double_quotes_in_strings() {
605        let query = r#"CREATE ISSUE IN test WITH TITLE "Double quoted string""#;
606        let result = parse_query(query);
607        assert!(result.is_ok());
608    }
609
610    #[test]
611    fn test_labels_with_hyphens() {
612        let query =
613            "CREATE ISSUE IN test WITH TITLE 'Test' LABELS [high-priority, bug-fix, ui-component]";
614        let result = parse_query(query);
615        assert!(result.is_ok());
616    }
617
618    #[test]
619    fn test_complex_real_world_query() {
620        let query = r#"
621            SELECT title, status, priority, assignee, created_at
622            FROM issues
623            WHERE (priority = critical OR priority = high)
624              AND status IN ('open', 'in-progress')
625              AND assignee IS NOT NULL
626              AND project = 'backend'
627            ORDER BY priority DESC
628            LIMIT 25
629            OFFSET 0
630        "#;
631        let result = parse_query(query);
632        assert!(result.is_ok());
633    }
634
635    #[test]
636    fn test_minimal_create_user() {
637        let query = "CREATE USER alice";
638        let result = parse_query(query);
639        assert!(result.is_ok());
640        if let Ok(Statement::Create(CreateStatement::User { email, name, .. })) = result {
641            assert!(email.is_none());
642            assert!(name.is_none());
643        }
644    }
645
646    #[test]
647    fn test_minimal_create_project() {
648        let query = "CREATE PROJECT test";
649        let result = parse_query(query);
650        assert!(result.is_ok());
651    }
652
653    #[test]
654    fn test_select_from_all_entities() {
655        for entity in &["users", "projects", "issues", "comments"] {
656            let query = format!("SELECT * FROM {}", entity);
657            let result = parse_query(&query);
658            assert!(
659                result.is_ok(),
660                "Failed query '{}' with error {}",
661                query,
662                result.err().unwrap()
663            );
664        }
665    }
666
667    #[test]
668    fn test_issue_id_variations() {
669        let queries = vec![
670            "CLOSE issue a#1",
671            "CLOSE issue my-project#123",
672            "CLOSE issue backend_api#456",
673        ];
674        for query in queries {
675            let result = parse_query(query);
676            assert!(
677                result.is_ok(),
678                "Failed query '{}' with error {}",
679                query,
680                result.err().unwrap()
681            );
682        }
683    }
684
685    #[test]
686    fn test_priority_in_different_cases() {
687        let queries = vec![
688            "CREATE ISSUE IN test WITH TITLE 'T' PRIORITY critical",
689            "CREATE ISSUE IN test WITH TITLE 'T' PRIORITY CRITICAL",
690            "CREATE ISSUE IN test WITH TITLE 'T' PRIORITY Critical",
691        ];
692        for query in queries {
693            let result = parse_query(query);
694            assert!(
695                result.is_ok(),
696                "Failed query '{}' with error {}",
697                query,
698                result.err().unwrap()
699            );
700        }
701    }
702
703    #[test]
704    fn test_all_comparison_ops_with_strings() {
705        let query = "SELECT * FROM issues WHERE title LIKE '%bug%'";
706        let result = parse_query(query);
707        assert!(result.is_ok());
708    }
709
710    #[test]
711    fn test_single_column_select() {
712        let query = "SELECT title FROM issues";
713        let result = parse_query(query);
714        assert!(result.is_ok());
715        if let Ok(Statement::Select(select)) = result {
716            assert_eq!(select.columns.count(), 1);
717        }
718    }
719
720    #[test]
721    fn test_whitespace_variations() {
722        let queries = vec![
723            "SELECT * FROM issues",
724            "SELECT  *  FROM  issues",
725            "SELECT\t*\tFROM\tissues",
726            "SELECT\n*\nFROM\nissues",
727        ];
728        for query in queries {
729            let result = parse_query(query);
730            assert!(
731                result.is_ok(),
732                "Failed query '{}' with error {}",
733                query,
734                result.err().unwrap()
735            );
736        }
737    }
738
739    #[test]
740    fn test_field_update_with_priority() {
741        let query = "UPDATE issue backend#1 SET priority = critical, status = 'open'";
742        let result = parse_query(query);
743        assert!(result.is_ok());
744    }
745
746    #[test]
747    fn test_field_update_with_identifier() {
748        let query = "UPDATE issue backend#1 SET assignee = alice, project = backend";
749        let result = parse_query(query);
750        assert!(result.is_ok());
751    }
752}