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<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}