Skip to main content

radixdb_sql/token/
mod.rs

1// Copyright 2026 RadixDB Contributors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Token types for SQL lexer
16//!
17//! This module defines the token types used by the SQL lexer and parser.
18
19use radixdb_core::SmartString;
20use rustc_hash::FxHashSet;
21use std::fmt;
22use std::sync::LazyLock;
23
24/// Position represents a position in the input source
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
26pub struct Position {
27    /// Byte offset, starting at 0
28    pub offset: usize,
29    /// Line number, starting at 1
30    pub line: usize,
31    /// Column number, starting at 1
32    pub column: usize,
33}
34
35impl Position {
36    /// Create a new position
37    pub fn new(offset: usize, line: usize, column: usize) -> Self {
38        Self {
39            offset,
40            line,
41            column,
42        }
43    }
44}
45
46impl fmt::Display for Position {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        write!(f, "line {}, column {}", self.line, self.column)
49    }
50}
51
52/// TokenType represents the type of a token
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
54pub enum TokenType {
55    /// Error token
56    Error,
57    /// End of file
58    Eof,
59    /// Identifier (table name, column name, etc.)
60    Identifier,
61    /// SQL keyword (SELECT, FROM, WHERE, etc.)
62    Keyword,
63    /// String literal ('hello')
64    String,
65    /// Integer number (123)
66    Integer,
67    /// Floating point number (123.45)
68    Float,
69    /// Operator (=, <, >, +, -, etc.)
70    Operator,
71    /// Punctuator (comma, semicolon, parentheses, etc.)
72    Punctuator,
73    /// Comment (-- or /* */)
74    Comment,
75    /// Parameter ($1, ?)
76    Parameter,
77}
78
79impl fmt::Display for TokenType {
80    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81        match self {
82            TokenType::Error => write!(f, "ERROR"),
83            TokenType::Eof => write!(f, "EOF"),
84            TokenType::Identifier => write!(f, "IDENTIFIER"),
85            TokenType::Keyword => write!(f, "KEYWORD"),
86            TokenType::String => write!(f, "STRING"),
87            TokenType::Integer => write!(f, "INTEGER"),
88            TokenType::Float => write!(f, "FLOAT"),
89            TokenType::Operator => write!(f, "OPERATOR"),
90            TokenType::Punctuator => write!(f, "PUNCTUATOR"),
91            TokenType::Comment => write!(f, "COMMENT"),
92            TokenType::Parameter => write!(f, "PARAMETER"),
93        }
94    }
95}
96
97/// Token represents a lexical token
98///
99/// Uses SmartString for literal to avoid heap allocation for tokens up to 24 bytes
100/// (covers most SQL tokens: keywords, operators, short identifiers, numbers).
101/// For error tokens, the literal field contains the error message.
102#[derive(Debug, Clone, PartialEq)]
103pub struct Token {
104    /// The type of the token
105    pub token_type: TokenType,
106    /// The literal string value (SmartString inlines strings up to 24 bytes)
107    /// For error tokens, this contains the error message instead.
108    pub literal: SmartString,
109    /// The position in the source
110    pub position: Position,
111    /// Whether this token was double-quoted (identifier with string fallback)
112    pub quoted: bool,
113}
114
115impl Token {
116    /// Create a new token
117    #[inline]
118    pub fn new(token_type: TokenType, literal: impl AsRef<str>, position: Position) -> Self {
119        Self {
120            token_type,
121            literal: SmartString::from(literal.as_ref()),
122            position,
123            quoted: false,
124        }
125    }
126
127    /// Create a new double-quoted token (identifier with string fallback)
128    #[inline]
129    pub fn new_quoted(token_type: TokenType, literal: impl AsRef<str>, position: Position) -> Self {
130        Self {
131            token_type,
132            literal: SmartString::from(literal.as_ref()),
133            position,
134            quoted: true,
135        }
136    }
137
138    /// Create an error token (error message is stored in literal field)
139    pub fn error(message: impl AsRef<str>, _literal: impl AsRef<str>, position: Position) -> Self {
140        Self {
141            token_type: TokenType::Error,
142            literal: SmartString::from(message.as_ref()),
143            position,
144            quoted: false,
145        }
146    }
147
148    /// Create an EOF token
149    #[inline]
150    pub fn eof(position: Position) -> Self {
151        Self {
152            token_type: TokenType::Eof,
153            literal: SmartString::const_new(""),
154            position,
155            quoted: false,
156        }
157    }
158
159    /// Check if this is an EOF token
160    pub fn is_eof(&self) -> bool {
161        self.token_type == TokenType::Eof
162    }
163
164    /// Check if this is an error token
165    pub fn is_error(&self) -> bool {
166        self.token_type == TokenType::Error
167    }
168
169    /// Check if this is a keyword with the given value (case-insensitive)
170    pub fn is_keyword(&self, keyword: &str) -> bool {
171        self.token_type == TokenType::Keyword && self.literal.eq_ignore_ascii_case(keyword)
172    }
173
174    /// Check if this is an operator with the given value
175    pub fn is_operator(&self, op: &str) -> bool {
176        self.token_type == TokenType::Operator && self.literal == op
177    }
178
179    /// Check if this is a punctuator with the given value
180    pub fn is_punctuator(&self, punct: &str) -> bool {
181        self.token_type == TokenType::Punctuator && self.literal == punct
182    }
183}
184
185impl fmt::Display for Token {
186    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187        if self.token_type == TokenType::Error {
188            // For error tokens, literal contains the error message
189            write!(
190                f,
191                "{}: {} at {}",
192                self.token_type, self.literal, self.position
193            )
194        } else if self.token_type == TokenType::Keyword {
195            write!(
196                f,
197                "{}: {} at {}",
198                self.token_type, self.literal, self.position
199            )
200        } else {
201            write!(
202                f,
203                "{}: '{}' at {}",
204                self.token_type, self.literal, self.position
205            )
206        }
207    }
208}
209
210/// SQL keywords (case-insensitive)
211pub static KEYWORDS: &[&str] = &[
212    "SELECT",
213    "FROM",
214    "WHERE",
215    "INSERT",
216    "INTO",
217    "VALUES",
218    "UPDATE",
219    "SET",
220    "DELETE",
221    "CREATE",
222    "REPLACE",
223    "TABLE",
224    "DROP",
225    "ALTER",
226    "ADD",
227    "COLUMN",
228    "AND",
229    "OR",
230    "XOR",
231    "NOT",
232    "NULL",
233    "PRIMARY",
234    "PRAGMA",
235    "KEY",
236    "AUTO_INCREMENT",
237    "AUTOINCREMENT",
238    "DEFAULT",
239    "AS",
240    "OF",
241    "DISTINCT",
242    "ORDER",
243    "BY",
244    "ASC",
245    "DESC",
246    "LIMIT",
247    "OFFSET",
248    "GROUP",
249    "HAVING",
250    "JOIN",
251    "INNER",
252    "OUTER",
253    "LEFT",
254    "RIGHT",
255    "FULL",
256    "ON",
257    "DUPLICATE",
258    "CONFLICT",
259    "DO",
260    "NOTHING",
261    "USING",
262    "CROSS",
263    "NATURAL",
264    "TRUE",
265    "FALSE",
266    "INTEGER",
267    "FLOAT",
268    "TEXT",
269    "BOOLEAN",
270    "BOOL",
271    "TIMESTAMP",
272    "TIMESTAMPTZ",
273    "DATETIME",
274    "DATE",
275    "TIME",
276    "JSON",
277    "UUID",
278    "VECTOR",
279    "CASE",
280    "CAST",
281    "EXTRACT",
282    "WHEN",
283    "THEN",
284    "ELSE",
285    "END",
286    "BETWEEN",
287    "IN",
288    "IS",
289    "LIKE",
290    "ILIKE",
291    "ESCAPE",
292    "GLOB",
293    "REGEXP",
294    "RLIKE",
295    "EXISTS",
296    "ALL",
297    "ANY",
298    "SOME",
299    "IF",
300    "UNION",
301    "INTERSECT",
302    "EXCEPT",
303    "WITH",
304    "UNIQUE",
305    "CHECK",
306    "CONSTRAINT",
307    "FOREIGN",
308    "REFERENCES",
309    "SHOW",
310    "DESCRIBE",
311    "DESC",
312    "TABLES",
313    "VIEWS",
314    "INDEXES",
315    "CASCADE",
316    "RESTRICT",
317    "INDEX",
318    "VIEW",
319    "EXTENSION",
320    "TYPE",
321    "VERSION",
322    "TRIGGER",
323    "PROCEDURE",
324    "FUNCTION",
325    "RETURNING",
326    "OVER",
327    "PARTITION",
328    "RANGE",
329    "ROWS",
330    "WINDOW",
331    "UNBOUNDED",
332    "BEGIN",
333    "TRANSACTION",
334    "COMMIT",
335    "ROLLBACK",
336    "SAVEPOINT",
337    "RELEASE",
338    "PRECEDING",
339    "FOLLOWING",
340    "CURRENT",
341    "ROW",
342    "MODIFY",
343    "RENAME",
344    "TO",
345    "VARCHAR",
346    "CHAR",
347    "STRING",
348    "BIGINT",
349    "TINYINT",
350    "SMALLINT",
351    "REAL",
352    "DOUBLE",
353    "DECIMAL",
354    "NUMERIC",
355    "INT",
356    "ISOLATION",
357    "LEVEL",
358    "READ",
359    "COMMITTED",
360    "UNCOMMITTED",
361    "INTERVAL",
362    "RECURSIVE",
363    "UNION",
364    "INTERSECT",
365    "EXCEPT",
366    "NULLS",
367    "FIRST",
368    "LAST",
369    "TRUNCATE",
370    "SOME",
371    "FILTER",
372    "RETURNING",
373    "EXPLAIN",
374    "ANALYZE",
375    "FETCH",
376    "NEXT",
377    "ONLY",
378    "VACUUM",
379    "COPY",
380    "FORMAT",
381    "HEADER",
382    "DELIMITER",
383    "DECLARE",
384    "CURSOR",
385    "CONSTANT",
386    "ELSIF",
387    "THEN",
388    "LOOP",
389    "WHILE",
390    "FOR",
391    "REVERSE",
392    "EXIT",
393    "CONTINUE",
394    "RETURN",
395    "QUERY",
396    "EXCEPTION",
397    "RAISE",
398    "OTHERS",
399    "OPEN",
400    "CLOSE",
401    "CALL",
402    "PERFORM",
403    "EXECUTE",
404    "FUNCTION",
405    "PROCEDURE",
406    "RETURNS",
407    "LANGUAGE",
408    "RADIX",
409    "NATIVE",
410    "OUT",
411    "INOUT",
412    "ARRAY",
413    "ROWTYPE",
414    "IMMUTABLE",
415    "STABLE",
416    "VOLATILE",
417    "SECURITY",
418    "INVOKER",
419    "DEFINER",
420    "SEARCH",
421    "PATH",
422    "RESOURCE",
423    "POLICY",
424    "STRICT",
425    "PRIORITY",
426    "BEFORE",
427    "AFTER",
428    "EACH",
429    "STATEMENT",
430    "OLD",
431    "NEW",
432    "JOB",
433    "SCHEDULE",
434    "EVERY",
435    "AT",
436    "ENABLE",
437    "DISABLE",
438    "RUN",
439    "PRINCIPAL",
440    "PASSWORD",
441    "ROLE",
442    "SCHEMA",
443    "DATABASE",
444    "GRANT",
445    "REVOKE",
446    "CONNECT",
447    "USAGE",
448    "OWNER",
449    "ADMIN",
450    "OPTION",
451    "FOUND",
452    "NOTFOUND",
453    "ROWCOUNT",
454    "ISOPEN",
455    "OPERATOR",
456    "CLASS",
457    "PLANNER",
458    "SUPPORT",
459    "LEFTARG",
460    "RIGHTARG",
461];
462
463/// Compiled keyword set for O(1) lookups
464/// Uses FxHashSet for fast hashing of short strings
465static KEYWORD_SET: LazyLock<FxHashSet<&'static str>> = LazyLock::new(|| {
466    let mut set = FxHashSet::with_capacity_and_hasher(KEYWORDS.len(), Default::default());
467    for kw in KEYWORDS {
468        set.insert(*kw);
469    }
470    set
471});
472
473/// Check if a string is an SQL keyword (case-insensitive)
474/// Uses a compiled HashSet for O(1) lookups instead of O(n) linear search
475#[inline]
476pub fn is_keyword(s: &str) -> bool {
477    // Fast path: check if already uppercase and in set
478    if KEYWORD_SET.contains(s) {
479        return true;
480    }
481    // Slow path: uppercase and check (only for non-uppercase input)
482    // Use a stack buffer for small strings to avoid allocation
483    if s.len() <= 32 {
484        let mut buf = [0u8; 32];
485        let bytes = s.as_bytes();
486        for (i, &b) in bytes.iter().enumerate() {
487            buf[i] = b.to_ascii_uppercase();
488        }
489        // SAFETY: We only uppercased ASCII bytes, result is valid UTF-8
490        let upper = unsafe { std::str::from_utf8_unchecked(&buf[..s.len()]) };
491        KEYWORD_SET.contains(upper)
492    } else {
493        // Very long identifiers (rare) - fall back to allocation
494        let upper = s.to_uppercase();
495        KEYWORD_SET.contains(upper.as_str())
496    }
497}
498
499/// SQL operators
500pub static OPERATORS: &[&str] = &[
501    "=", ">", "<", ">=", "<=", "<>", "!=", "+", "-", "*", "/", "%",
502    "||", // String concatenation
503    "->", "->>", // JSON operators
504    "&", "|", "^", "~", "<<", ">>",  // Bitwise operators
505    "<=>", // Vector distance operator
506    "&&", "@>", "<@", // Extension operator alphabet
507    ":=", "=>",
508];
509
510/// Compiled operator set for O(1) lookups
511static OPERATOR_SET: LazyLock<FxHashSet<&'static str>> = LazyLock::new(|| {
512    let mut set = FxHashSet::with_capacity_and_hasher(OPERATORS.len(), Default::default());
513    for op in OPERATORS {
514        set.insert(*op);
515    }
516    set
517});
518
519/// Check if a string is an SQL operator
520#[inline]
521pub fn is_operator(s: &str) -> bool {
522    OPERATOR_SET.contains(s)
523}
524
525/// SQL punctuators
526pub static PUNCTUATORS: &[char] = &[',', ';', '(', ')', '.', ':', '[', ']'];
527
528/// Check if a character is an SQL punctuator
529pub fn is_punctuator(c: char) -> bool {
530    PUNCTUATORS.contains(&c)
531}
532
533/// Get a static string for a punctuator character (avoids allocation)
534/// Returns None if the character is not a punctuator
535#[inline]
536pub fn punctuator_str(c: char) -> Option<&'static str> {
537    match c {
538        ',' => Some(","),
539        ';' => Some(";"),
540        '(' => Some("("),
541        ')' => Some(")"),
542        '.' => Some("."),
543        ':' => Some(":"),
544        '[' => Some("["),
545        ']' => Some("]"),
546        _ => None,
547    }
548}
549
550/// Characters that can be part of an operator
551pub fn is_operator_char(c: char) -> bool {
552    // `#` is a comment prefix.
553    matches!(
554        c,
555        '=' | '<' | '>' | '!' | '+' | '-' | '*' | '/' | '%' | '|' | '&' | '^' | '~' | ':' | '@'
556    )
557}
558
559#[cfg(test)]
560mod tests {
561    use super::*;
562
563    #[test]
564    fn test_position_display() {
565        let pos = Position::new(10, 2, 5);
566        assert_eq!(pos.to_string(), "line 2, column 5");
567    }
568
569    #[test]
570    fn test_token_type_display() {
571        assert_eq!(TokenType::Keyword.to_string(), "KEYWORD");
572        assert_eq!(TokenType::Identifier.to_string(), "IDENTIFIER");
573        assert_eq!(TokenType::String.to_string(), "STRING");
574        assert_eq!(TokenType::Eof.to_string(), "EOF");
575    }
576
577    #[test]
578    fn test_token_creation() {
579        let token = Token::new(TokenType::Keyword, "SELECT", Position::new(0, 1, 1));
580        assert_eq!(token.token_type, TokenType::Keyword);
581        assert_eq!(token.literal, "SELECT");
582        assert!(token.is_keyword("SELECT"));
583        assert!(token.is_keyword("select"));
584        assert!(!token.is_keyword("FROM"));
585    }
586
587    #[test]
588    fn test_error_token() {
589        let token = Token::error("unexpected character", "x", Position::new(5, 1, 6));
590        assert!(token.is_error());
591        // Error message is stored in literal field
592        assert_eq!(token.literal.as_str(), "unexpected character");
593    }
594
595    #[test]
596    fn test_eof_token() {
597        let token = Token::eof(Position::new(100, 5, 10));
598        assert!(token.is_eof());
599        assert_eq!(token.literal, "");
600    }
601
602    #[test]
603    fn test_is_keyword() {
604        assert!(is_keyword("SELECT"));
605        assert!(is_keyword("select"));
606        assert!(is_keyword("Select"));
607        assert!(!is_keyword("SELEC"));
608        assert!(!is_keyword("mycolumn"));
609    }
610
611    #[test]
612    fn test_is_operator() {
613        assert!(is_operator("="));
614        assert!(is_operator(">="));
615        assert!(is_operator("->"));
616        assert!(is_operator("->>"));
617        assert!(!is_operator("==="));
618    }
619
620    #[test]
621    fn test_is_punctuator() {
622        assert!(is_punctuator(','));
623        assert!(is_punctuator(';'));
624        assert!(is_punctuator('('));
625        assert!(is_punctuator(')'));
626        assert!(!is_punctuator('x'));
627    }
628
629    #[test]
630    fn test_is_operator_char() {
631        assert!(is_operator_char('='));
632        assert!(is_operator_char('+'));
633        assert!(is_operator_char('-'));
634        assert!(is_operator_char('|'));
635        assert!(!is_operator_char('a'));
636        assert!(!is_operator_char('#')); // # is for comments, not operators
637    }
638
639    #[test]
640    fn test_token_display() {
641        let keyword = Token::new(TokenType::Keyword, "SELECT", Position::new(0, 1, 1));
642        assert!(keyword.to_string().contains("KEYWORD: SELECT"));
643
644        let string = Token::new(TokenType::String, "hello", Position::new(7, 1, 8));
645        assert!(string.to_string().contains("STRING: 'hello'"));
646
647        let error = Token::error("bad token", "x", Position::new(0, 1, 1));
648        assert!(error.to_string().contains("ERROR: bad token"));
649    }
650}