Skip to main content

grafeo_adapters/query/
keywords.rs

1//! Shared keyword definitions for graph query language parsers.
2//!
3//! Keywords are categorized as:
4//! - **Common**: shared across GQL, Cypher, and SQL/PGQ
5//! - **Language-specific**: unique to a single parser
6//!
7//! Each lexer calls `CommonKeyword::from_str` for shared keyword
8//! recognition, then maps the result to its own `TokenKind`.
9//! Language-specific keywords remain in each lexer.
10
11/// A keyword shared across multiple query language parsers.
12///
13/// Each parser maps these to its own `TokenKind` enum.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum CommonKeyword {
16    // Query structure
17    /// The `MATCH` keyword.
18    Match,
19    /// The `RETURN` keyword.
20    Return,
21    /// The `WHERE` keyword.
22    Where,
23    /// The `AS` keyword.
24    As,
25    /// The `DISTINCT` keyword.
26    Distinct,
27    /// The `WITH` keyword.
28    With,
29    /// The `OPTIONAL` keyword.
30    Optional,
31
32    // Ordering & pagination
33    /// The `ORDER` keyword.
34    Order,
35    /// The `BY` keyword.
36    By,
37    /// The `ASC` keyword.
38    Asc,
39    /// The `DESC` keyword.
40    Desc,
41    /// The `LIMIT` keyword.
42    Limit,
43    /// The `SKIP` keyword.
44    Skip,
45
46    // Logical operators
47    /// The `AND` keyword.
48    And,
49    /// The `OR` keyword.
50    Or,
51    /// The `NOT` keyword.
52    Not,
53
54    // Comparison
55    /// The `IN` keyword.
56    In,
57    /// The `IS` keyword.
58    Is,
59    /// The `LIKE` keyword.
60    Like,
61
62    // String predicates
63    /// The `STARTS` keyword.
64    Starts,
65    /// The `ENDS` keyword.
66    Ends,
67    /// The `CONTAINS` keyword.
68    Contains,
69
70    // Literals
71    /// The `NULL` keyword.
72    Null,
73    /// The `TRUE` keyword.
74    True,
75    /// The `FALSE` keyword.
76    False,
77
78    // Mutation
79    /// The `CREATE` keyword.
80    Create,
81    /// The `DELETE` keyword.
82    Delete,
83    /// The `SET` keyword.
84    Set,
85    /// The `REMOVE` keyword.
86    Remove,
87    /// The `MERGE` keyword.
88    Merge,
89    /// The `DETACH` keyword.
90    Detach,
91    /// The `ON` keyword.
92    On,
93
94    // Subquery / procedure
95    /// The `CALL` keyword.
96    Call,
97    /// The `YIELD` keyword.
98    Yield,
99    /// The `EXISTS` keyword.
100    Exists,
101    /// The `UNWIND` keyword.
102    Unwind,
103
104    // Graph structure
105    /// The `NODE` keyword.
106    Node,
107    /// The `EDGE` keyword.
108    Edge,
109
110    // Aggregate / grouping
111    /// The `HAVING` keyword.
112    Having,
113    /// The `CASE` keyword.
114    Case,
115    /// The `WHEN` keyword.
116    When,
117    /// The `THEN` keyword.
118    Then,
119    /// The `ELSE` keyword.
120    Else,
121    /// The `END` keyword.
122    End,
123}
124
125impl CommonKeyword {
126    /// Recognizes a keyword from its uppercase text.
127    ///
128    /// Returns `None` for language-specific or unrecognized identifiers.
129    /// The caller should convert the input to uppercase before calling.
130    #[must_use]
131    pub fn from_uppercase(text: &str) -> Option<Self> {
132        match text {
133            // Query structure
134            "MATCH" => Some(Self::Match),
135            "RETURN" => Some(Self::Return),
136            "WHERE" => Some(Self::Where),
137            "AS" => Some(Self::As),
138            "DISTINCT" => Some(Self::Distinct),
139            "WITH" => Some(Self::With),
140            "OPTIONAL" => Some(Self::Optional),
141
142            // Ordering & pagination
143            "ORDER" => Some(Self::Order),
144            "BY" => Some(Self::By),
145            "ASC" => Some(Self::Asc),
146            "DESC" => Some(Self::Desc),
147            "LIMIT" => Some(Self::Limit),
148            "SKIP" => Some(Self::Skip),
149
150            // Logical
151            "AND" => Some(Self::And),
152            "OR" => Some(Self::Or),
153            "NOT" => Some(Self::Not),
154
155            // Comparison
156            "IN" => Some(Self::In),
157            "IS" => Some(Self::Is),
158            "LIKE" => Some(Self::Like),
159
160            // String predicates
161            "STARTS" => Some(Self::Starts),
162            "ENDS" => Some(Self::Ends),
163            "CONTAINS" => Some(Self::Contains),
164
165            // Literals
166            "NULL" => Some(Self::Null),
167            "TRUE" => Some(Self::True),
168            "FALSE" => Some(Self::False),
169
170            // Mutation
171            "CREATE" => Some(Self::Create),
172            "DELETE" => Some(Self::Delete),
173            "SET" => Some(Self::Set),
174            "REMOVE" => Some(Self::Remove),
175            "MERGE" => Some(Self::Merge),
176            "DETACH" => Some(Self::Detach),
177            "ON" => Some(Self::On),
178
179            // Subquery / procedure
180            "CALL" => Some(Self::Call),
181            "YIELD" => Some(Self::Yield),
182            "EXISTS" => Some(Self::Exists),
183            "UNWIND" => Some(Self::Unwind),
184
185            // Graph structure
186            "NODE" => Some(Self::Node),
187            "EDGE" => Some(Self::Edge),
188
189            // Aggregate / grouping
190            "HAVING" => Some(Self::Having),
191            "CASE" => Some(Self::Case),
192            "WHEN" => Some(Self::When),
193            "THEN" => Some(Self::Then),
194            "ELSE" => Some(Self::Else),
195            "END" => Some(Self::End),
196
197            _ => None,
198        }
199    }
200
201    /// Returns true if the given uppercase text is a keyword in any parser.
202    #[must_use]
203    pub fn is_keyword(text: &str) -> bool {
204        Self::from_uppercase(text).is_some()
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn test_common_keywords_recognized() {
214        // Query structure
215        assert_eq!(
216            CommonKeyword::from_uppercase("MATCH"),
217            Some(CommonKeyword::Match)
218        );
219        assert_eq!(
220            CommonKeyword::from_uppercase("RETURN"),
221            Some(CommonKeyword::Return)
222        );
223        assert_eq!(
224            CommonKeyword::from_uppercase("WHERE"),
225            Some(CommonKeyword::Where)
226        );
227
228        // Logical
229        assert_eq!(
230            CommonKeyword::from_uppercase("AND"),
231            Some(CommonKeyword::And)
232        );
233        assert_eq!(CommonKeyword::from_uppercase("OR"), Some(CommonKeyword::Or));
234        assert_eq!(
235            CommonKeyword::from_uppercase("NOT"),
236            Some(CommonKeyword::Not)
237        );
238
239        // Literals
240        assert_eq!(
241            CommonKeyword::from_uppercase("NULL"),
242            Some(CommonKeyword::Null)
243        );
244        assert_eq!(
245            CommonKeyword::from_uppercase("TRUE"),
246            Some(CommonKeyword::True)
247        );
248        assert_eq!(
249            CommonKeyword::from_uppercase("FALSE"),
250            Some(CommonKeyword::False)
251        );
252    }
253
254    #[test]
255    fn test_non_keywords_return_none() {
256        assert_eq!(CommonKeyword::from_uppercase("FOOBAR"), None);
257        assert_eq!(CommonKeyword::from_uppercase("person"), None);
258        assert_eq!(CommonKeyword::from_uppercase(""), None);
259    }
260
261    #[test]
262    fn test_is_keyword() {
263        assert!(CommonKeyword::is_keyword("MATCH"));
264        assert!(CommonKeyword::is_keyword("WHERE"));
265        assert!(!CommonKeyword::is_keyword("FOOBAR"));
266    }
267
268    #[test]
269    fn test_language_specific_not_common() {
270        // SQL/PGQ specific
271        assert_eq!(CommonKeyword::from_uppercase("SELECT"), None);
272        assert_eq!(CommonKeyword::from_uppercase("FROM"), None);
273        assert_eq!(CommonKeyword::from_uppercase("GRAPH_TABLE"), None);
274
275        // Cypher specific
276        assert_eq!(CommonKeyword::from_uppercase("UNION"), None);
277        assert_eq!(CommonKeyword::from_uppercase("XOR"), None);
278
279        // GQL specific
280        assert_eq!(CommonKeyword::from_uppercase("VECTOR"), None);
281        assert_eq!(CommonKeyword::from_uppercase("INDEX"), None);
282    }
283
284    #[test]
285    fn test_all_common_keywords_covered() {
286        let all_keywords = [
287            "MATCH", "RETURN", "WHERE", "AS", "DISTINCT", "WITH", "OPTIONAL", "ORDER", "BY", "ASC",
288            "DESC", "LIMIT", "SKIP", "AND", "OR", "NOT", "IN", "IS", "LIKE", "STARTS", "ENDS",
289            "CONTAINS", "NULL", "TRUE", "FALSE", "CREATE", "DELETE", "SET", "REMOVE", "MERGE",
290            "DETACH", "ON", "CALL", "YIELD", "EXISTS", "UNWIND", "NODE", "EDGE", "HAVING", "CASE",
291            "WHEN", "THEN", "ELSE", "END",
292        ];
293
294        for kw in &all_keywords {
295            assert!(
296                CommonKeyword::from_uppercase(kw).is_some(),
297                "Expected '{kw}' to be recognized as a common keyword"
298            );
299        }
300    }
301}