Skip to main content

reddb_rql/
lexer.rs

1//! RQL Lexer
2//!
3//! Tokenizes RQL (RedDB Query Language) strings for parsing.
4//! Supports both SQL-like table queries and Cypher-like graph patterns.
5//!
6//! # Token Types
7//!
8//! - Keywords: SELECT, FROM, WHERE, MATCH, RETURN, JOIN, GRAPH, PATH, etc.
9//! - Literals: strings, integers, floats, booleans
10//! - Identifiers: table names, column names, aliases
11//! - Operators: comparison, arithmetic, logical
12//! - Graph syntax: arrows (->), edge brackets ([-])
13
14use std::fmt;
15use std::iter::Peekable;
16use std::str::Chars;
17
18/// Token types for RQL
19#[derive(Debug, Clone, PartialEq)]
20pub enum Token {
21    // Keywords
22    Select,
23    From,
24    Where,
25    And,
26    Or,
27    Not,
28    Match,
29    Return,
30    Join,
31    Graph,
32    Path,
33    To,
34    Via,
35    On,
36    As,
37    Is,
38    Null,
39    Between,
40    Like,
41    In,
42    Order,
43    By,
44    Asc,
45    Desc,
46    Nulls,
47    First,
48    Last,
49    Limit,
50    Offset,
51    Inner,
52    Left,
53    Right,
54    Outer,
55    Full,
56    Cross,
57    Starts,
58    Ends,
59    With,
60    Contains,
61    True,
62    False,
63    Enrich,
64    Group,
65    Count,
66    Sum,
67    Avg,
68    Min,
69    Max,
70    Distinct,
71
72    // Vector query keywords
73    Vector,
74    Search,
75    Similar,
76    Collection,
77    Metric,
78    Threshold,
79    K,
80    Hybrid,
81    Fusion,
82    Rerank,
83    Rrf,
84    Intersection,
85    Union,
86    Recursive,
87    All,
88    Weight,
89    L2,
90    Cosine,
91    InnerProduct,
92    Include,
93    Metadata,
94    Vectors,
95
96    // DML/DDL keywords
97    Insert,
98    Into,
99    Values,
100    Update,
101    Set,
102    Delete,
103    Truncate,
104    Create,
105    Table,
106    Drop,
107    Alter,
108    Add,
109    Column,
110    Primary,
111    // EXPLAIN ALTER FOR — schema diff command
112    Explain,
113    For,
114    Format,
115    Json,
116    Key,
117    Default,
118    Compress,
119    Index,
120    Unique,
121    If,
122    Exists,
123    Returning,
124    Cascade,
125    Rename,
126    Using,
127
128    // Entity type keywords
129    Node,
130    Edge,
131    Document,
132    Kv,
133
134    // Time-series & Queue keywords
135    Timeseries,
136    Retention,
137    Queue,
138    Tree,
139    Push,
140    Pop,
141    Peek,
142    Purge,
143    Ack,
144    Nack,
145    Priority,
146
147    // Graph command keywords
148    Neighborhood,
149    ShortestPath,
150    Centrality,
151    Community,
152    Components,
153    Cycles,
154    Traverse,
155    Depth,
156    Direction,
157    Algorithm,
158    Strategy,
159    MaxIterations,
160    MaxLength,
161    Mode,
162    Clustering,
163    TopologicalSort,
164    Properties,
165    Text,
166    Fuzzy,
167    MinScore,
168
169    // Transaction control keywords (Phase 1.1 PG parity)
170    Begin,
171    Commit,
172    Rollback,
173    Savepoint,
174    Release,
175    Start,
176    Transaction,
177    Work,
178
179    // Maintenance keywords (Phase 1.2 PG parity)
180    Vacuum,
181    Analyze,
182
183    // Schema + sequence keywords (Phase 1.3 PG parity)
184    Schema,
185    Sequence,
186    Increment,
187
188    // COPY command keywords (Phase 1.5 PG parity)
189    Copy,
190    Header,
191    Delimiter,
192
193    // View keywords (Phase 2.1 PG parity)
194    View,
195    Materialized,
196    Refresh,
197
198    // Partitioning keywords (Phase 2.2 PG parity)
199    Partition,
200    Range,
201    List,
202    Hash,
203    Attach,
204    Detach,
205    Of,
206
207    // Row Level Security keywords (Phase 2.5 PG parity)
208    Policy,
209    Enable,
210    Disable,
211    Security,
212    Row,
213    Level,
214
215    // Foreign Data Wrapper keywords (Phase 3.2 PG parity)
216    Foreign,
217    Server,
218    Wrapper,
219    Options,
220    Data,
221
222    // SESSIONIZE operator (issue #585 slice 8).
223    Sessionize,
224    Gap,
225
226    // Window OVER-clause keywords (issue #589 slice 7a).
227    // Note: PARTITION, RANGE, BETWEEN, ROW already exist for DDL/predicate
228    // contexts; reused here in window context.
229    Over,
230    Rows,
231    Preceding,
232    Following,
233    Unbounded,
234    Current,
235
236    // Literals
237    String(String),
238    Integer(i64),
239    Float(f64),
240    /// Raw JSON object literal text — produced when the lexer enters JSON
241    /// sub-mode at a `{` whose first non-whitespace inner char is `"`,
242    /// signalling a standard JSON object. The String holds the verbatim
243    /// `{...}` text, including the enclosing braces. The parser hands it
244    /// to `parse_json` to materialise a `Value::Json`. See issue #86.
245    JsonLiteral(String),
246
247    // Identifiers
248    Ident(String),
249
250    // Operators
251    Eq,      // =
252    Ne,      // <> or !=
253    Lt,      // <
254    Le,      // <=
255    Gt,      // >
256    Ge,      // >=
257    Plus,    // +
258    Minus,   // -
259    Star,    // *
260    Slash,   // /
261    Percent, // %
262
263    // Delimiters
264    LParen,   // (
265    RParen,   // )
266    LBracket, // [
267    RBracket, // ]
268    LBrace,   // {
269    RBrace,   // }
270    Comma,    // ,
271    Dot,      // .
272    Colon,    // :
273    Semi,     // ;
274    Dollar,   // $
275    Question, // ?
276
277    // Named-argument syntax (e.g. `louvain(g, resolution => 0.5)`)
278    FatArrow, // =>
279
280    // Graph syntax
281    Arrow,      // ->
282    ArrowLeft,  // <-
283    Dash,       // -
284    DotDot,     // ..
285    Pipe,       // |
286    DoublePipe, // ||
287
288    // End of input
289    Eof,
290}
291
292impl fmt::Display for Token {
293    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
294        match self {
295            Token::Select => write!(f, "SELECT"),
296            Token::From => write!(f, "FROM"),
297            Token::Where => write!(f, "WHERE"),
298            Token::And => write!(f, "AND"),
299            Token::Or => write!(f, "OR"),
300            Token::Not => write!(f, "NOT"),
301            Token::Match => write!(f, "MATCH"),
302            Token::Return => write!(f, "RETURN"),
303            Token::Join => write!(f, "JOIN"),
304            Token::Graph => write!(f, "GRAPH"),
305            Token::Path => write!(f, "PATH"),
306            Token::To => write!(f, "TO"),
307            Token::Via => write!(f, "VIA"),
308            Token::On => write!(f, "ON"),
309            Token::As => write!(f, "AS"),
310            Token::Is => write!(f, "IS"),
311            Token::Null => write!(f, "NULL"),
312            Token::Between => write!(f, "BETWEEN"),
313            Token::Like => write!(f, "LIKE"),
314            Token::In => write!(f, "IN"),
315            Token::Order => write!(f, "ORDER"),
316            Token::By => write!(f, "BY"),
317            Token::Asc => write!(f, "ASC"),
318            Token::Desc => write!(f, "DESC"),
319            Token::Nulls => write!(f, "NULLS"),
320            Token::First => write!(f, "FIRST"),
321            Token::Last => write!(f, "LAST"),
322            Token::Limit => write!(f, "LIMIT"),
323            Token::Offset => write!(f, "OFFSET"),
324            Token::Inner => write!(f, "INNER"),
325            Token::Left => write!(f, "LEFT"),
326            Token::Right => write!(f, "RIGHT"),
327            Token::Outer => write!(f, "OUTER"),
328            Token::Full => write!(f, "FULL"),
329            Token::Cross => write!(f, "CROSS"),
330            Token::Starts => write!(f, "STARTS"),
331            Token::Ends => write!(f, "ENDS"),
332            Token::With => write!(f, "WITH"),
333            Token::Contains => write!(f, "CONTAINS"),
334            Token::True => write!(f, "TRUE"),
335            Token::False => write!(f, "FALSE"),
336            Token::Enrich => write!(f, "ENRICH"),
337            Token::Group => write!(f, "GROUP"),
338            Token::Count => write!(f, "COUNT"),
339            Token::Sum => write!(f, "SUM"),
340            Token::Avg => write!(f, "AVG"),
341            Token::Min => write!(f, "MIN"),
342            Token::Max => write!(f, "MAX"),
343            Token::Distinct => write!(f, "DISTINCT"),
344            Token::Vector => write!(f, "VECTOR"),
345            Token::Search => write!(f, "SEARCH"),
346            Token::Similar => write!(f, "SIMILAR"),
347            Token::Collection => write!(f, "COLLECTION"),
348            Token::Metric => write!(f, "METRIC"),
349            Token::Threshold => write!(f, "THRESHOLD"),
350            Token::K => write!(f, "K"),
351            Token::Hybrid => write!(f, "HYBRID"),
352            Token::Fusion => write!(f, "FUSION"),
353            Token::Rerank => write!(f, "RERANK"),
354            Token::Rrf => write!(f, "RRF"),
355            Token::Intersection => write!(f, "INTERSECTION"),
356            Token::Union => write!(f, "UNION"),
357            Token::Recursive => write!(f, "RECURSIVE"),
358            Token::All => write!(f, "ALL"),
359            Token::Weight => write!(f, "WEIGHT"),
360            Token::L2 => write!(f, "L2"),
361            Token::Cosine => write!(f, "COSINE"),
362            Token::InnerProduct => write!(f, "INNER_PRODUCT"),
363            Token::Include => write!(f, "INCLUDE"),
364            Token::Metadata => write!(f, "METADATA"),
365            Token::Vectors => write!(f, "VECTORS"),
366            Token::Explain => write!(f, "EXPLAIN"),
367            Token::For => write!(f, "FOR"),
368            Token::Format => write!(f, "FORMAT"),
369            Token::Json => write!(f, "JSON"),
370            Token::Insert => write!(f, "INSERT"),
371            Token::Into => write!(f, "INTO"),
372            Token::Values => write!(f, "VALUES"),
373            Token::Update => write!(f, "UPDATE"),
374            Token::Set => write!(f, "SET"),
375            Token::Delete => write!(f, "DELETE"),
376            Token::Truncate => write!(f, "TRUNCATE"),
377            Token::Create => write!(f, "CREATE"),
378            Token::Table => write!(f, "TABLE"),
379            Token::Drop => write!(f, "DROP"),
380            Token::Alter => write!(f, "ALTER"),
381            Token::Add => write!(f, "ADD"),
382            Token::Column => write!(f, "COLUMN"),
383            Token::Primary => write!(f, "PRIMARY"),
384            Token::Key => write!(f, "KEY"),
385            Token::Default => write!(f, "DEFAULT"),
386            Token::Compress => write!(f, "COMPRESS"),
387            Token::Index => write!(f, "INDEX"),
388            Token::Unique => write!(f, "UNIQUE"),
389            Token::If => write!(f, "IF"),
390            Token::Exists => write!(f, "EXISTS"),
391            Token::Returning => write!(f, "RETURNING"),
392            Token::Cascade => write!(f, "CASCADE"),
393            Token::Rename => write!(f, "RENAME"),
394            Token::Using => write!(f, "USING"),
395            Token::Node => write!(f, "NODE"),
396            Token::Edge => write!(f, "EDGE"),
397            Token::Document => write!(f, "DOCUMENT"),
398            Token::Kv => write!(f, "KV"),
399            Token::Timeseries => write!(f, "TIMESERIES"),
400            Token::Retention => write!(f, "RETENTION"),
401            Token::Queue => write!(f, "QUEUE"),
402            Token::Tree => write!(f, "TREE"),
403            Token::Push => write!(f, "PUSH"),
404            Token::Pop => write!(f, "POP"),
405            Token::Peek => write!(f, "PEEK"),
406            Token::Purge => write!(f, "PURGE"),
407            Token::Ack => write!(f, "ACK"),
408            Token::Nack => write!(f, "NACK"),
409            Token::Priority => write!(f, "PRIORITY"),
410            Token::Neighborhood => write!(f, "NEIGHBORHOOD"),
411            Token::ShortestPath => write!(f, "SHORTEST_PATH"),
412            Token::Centrality => write!(f, "CENTRALITY"),
413            Token::Community => write!(f, "COMMUNITY"),
414            Token::Components => write!(f, "COMPONENTS"),
415            Token::Cycles => write!(f, "CYCLES"),
416            Token::Traverse => write!(f, "TRAVERSE"),
417            Token::Depth => write!(f, "DEPTH"),
418            Token::Direction => write!(f, "DIRECTION"),
419            Token::Algorithm => write!(f, "ALGORITHM"),
420            Token::Strategy => write!(f, "STRATEGY"),
421            Token::MaxIterations => write!(f, "MAX_ITERATIONS"),
422            Token::MaxLength => write!(f, "MAX_LENGTH"),
423            Token::Mode => write!(f, "MODE"),
424            Token::Clustering => write!(f, "CLUSTERING"),
425            Token::TopologicalSort => write!(f, "TOPOLOGICAL_SORT"),
426            Token::Properties => write!(f, "PROPERTIES"),
427            Token::Text => write!(f, "TEXT"),
428            Token::Fuzzy => write!(f, "FUZZY"),
429            Token::MinScore => write!(f, "MIN_SCORE"),
430            Token::Begin => write!(f, "BEGIN"),
431            Token::Commit => write!(f, "COMMIT"),
432            Token::Rollback => write!(f, "ROLLBACK"),
433            Token::Savepoint => write!(f, "SAVEPOINT"),
434            Token::Release => write!(f, "RELEASE"),
435            Token::Start => write!(f, "START"),
436            Token::Transaction => write!(f, "TRANSACTION"),
437            Token::Work => write!(f, "WORK"),
438            Token::Vacuum => write!(f, "VACUUM"),
439            Token::Analyze => write!(f, "ANALYZE"),
440            Token::Schema => write!(f, "SCHEMA"),
441            Token::Sequence => write!(f, "SEQUENCE"),
442            Token::Increment => write!(f, "INCREMENT"),
443            Token::Copy => write!(f, "COPY"),
444            Token::Header => write!(f, "HEADER"),
445            Token::Delimiter => write!(f, "DELIMITER"),
446            Token::View => write!(f, "VIEW"),
447            Token::Materialized => write!(f, "MATERIALIZED"),
448            Token::Refresh => write!(f, "REFRESH"),
449            Token::Partition => write!(f, "PARTITION"),
450            Token::Range => write!(f, "RANGE"),
451            Token::List => write!(f, "LIST"),
452            Token::Hash => write!(f, "HASH"),
453            Token::Attach => write!(f, "ATTACH"),
454            Token::Detach => write!(f, "DETACH"),
455            Token::Of => write!(f, "OF"),
456            Token::Policy => write!(f, "POLICY"),
457            Token::Enable => write!(f, "ENABLE"),
458            Token::Disable => write!(f, "DISABLE"),
459            Token::Security => write!(f, "SECURITY"),
460            Token::Row => write!(f, "ROW"),
461            Token::Level => write!(f, "LEVEL"),
462            Token::Foreign => write!(f, "FOREIGN"),
463            Token::Server => write!(f, "SERVER"),
464            Token::Wrapper => write!(f, "WRAPPER"),
465            Token::Options => write!(f, "OPTIONS"),
466            Token::Data => write!(f, "DATA"),
467            Token::Sessionize => write!(f, "SESSIONIZE"),
468            Token::Gap => write!(f, "GAP"),
469            Token::Over => write!(f, "OVER"),
470            Token::Rows => write!(f, "ROWS"),
471            Token::Preceding => write!(f, "PRECEDING"),
472            Token::Following => write!(f, "FOLLOWING"),
473            Token::Unbounded => write!(f, "UNBOUNDED"),
474            Token::Current => write!(f, "CURRENT"),
475            Token::String(s) => write!(f, "'{}'", s),
476            Token::Integer(n) => write!(f, "{}", n),
477            Token::Float(n) => write!(f, "{}", n),
478            Token::JsonLiteral(s) => write!(f, "{}", s),
479            Token::Ident(s) => write!(f, "{}", s),
480            Token::Eq => write!(f, "="),
481            Token::Ne => write!(f, "<>"),
482            Token::Lt => write!(f, "<"),
483            Token::Le => write!(f, "<="),
484            Token::Gt => write!(f, ">"),
485            Token::Ge => write!(f, ">="),
486            Token::Plus => write!(f, "+"),
487            Token::Minus => write!(f, "-"),
488            Token::Star => write!(f, "*"),
489            Token::Slash => write!(f, "/"),
490            Token::Percent => write!(f, "%"),
491            Token::LParen => write!(f, "("),
492            Token::RParen => write!(f, ")"),
493            Token::LBracket => write!(f, "["),
494            Token::RBracket => write!(f, "]"),
495            Token::LBrace => write!(f, "{{"),
496            Token::RBrace => write!(f, "}}"),
497            Token::Comma => write!(f, ","),
498            Token::Dot => write!(f, "."),
499            Token::Colon => write!(f, ":"),
500            Token::Semi => write!(f, ";"),
501            Token::Dollar => write!(f, "$"),
502            Token::Question => write!(f, "?"),
503            Token::FatArrow => write!(f, "=>"),
504            Token::Arrow => write!(f, "->"),
505            Token::ArrowLeft => write!(f, "<-"),
506            Token::Dash => write!(f, "-"),
507            Token::DotDot => write!(f, ".."),
508            Token::Pipe => write!(f, "|"),
509            Token::DoublePipe => write!(f, "||"),
510            Token::Eof => write!(f, "EOF"),
511        }
512    }
513}
514
515/// Position in source code
516#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
517pub struct Position {
518    /// Line number (1-indexed)
519    pub line: u32,
520    /// Column number (1-indexed)
521    pub column: u32,
522    /// Byte offset from start
523    pub offset: u32,
524}
525
526impl Position {
527    /// Create a new position
528    pub fn new(line: u32, column: u32, offset: u32) -> Self {
529        Self {
530            line,
531            column,
532            offset,
533        }
534    }
535}
536
537impl fmt::Display for Position {
538    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
539        write!(f, "{}:{}", self.line, self.column)
540    }
541}
542
543/// A token with its position in source
544#[derive(Debug, Clone)]
545pub struct Spanned {
546    /// The token
547    pub token: Token,
548    /// Start position
549    pub start: Position,
550    /// End position
551    pub end: Position,
552}
553
554impl Spanned {
555    /// Create a new spanned token
556    pub fn new(token: Token, start: Position, end: Position) -> Self {
557        Self { token, start, end }
558    }
559}
560
561/// Lexer error
562#[derive(Debug, Clone)]
563pub struct LexerError {
564    /// Error message
565    pub message: String,
566    /// Position where error occurred
567    pub position: Position,
568    /// Optional structured DoS-limit annotation. When set, the
569    /// `From<LexerError> for ParseError` conversion preserves this
570    /// kind so callers can pattern-match on the limit programmatically.
571    pub limit_hit: Option<LexerLimitHit>,
572}
573
574/// A specific DoS limit that the lexer refused to cross.
575#[derive(Debug, Clone, PartialEq, Eq)]
576pub enum LexerLimitHit {
577    /// Identifier length cap.
578    IdentifierTooLong {
579        limit_name: &'static str,
580        value: usize,
581    },
582}
583
584impl LexerError {
585    /// Create a new lexer error
586    pub fn new(message: impl Into<String>, position: Position) -> Self {
587        Self {
588            message: message.into(),
589            position,
590            limit_hit: None,
591        }
592    }
593
594    /// Create a lexer error tagged with a structured limit-hit kind.
595    pub(crate) fn with_limit(
596        message: impl Into<String>,
597        position: Position,
598        limit_hit: LexerLimitHit,
599    ) -> Self {
600        Self {
601            message: message.into(),
602            position,
603            limit_hit: Some(limit_hit),
604        }
605    }
606}
607
608impl fmt::Display for LexerError {
609    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
610        write!(f, "Lexer error at {}: {}", self.position, self.message)
611    }
612}
613
614impl std::error::Error for LexerError {}
615
616/// Maximum byte size of a raw JSON object literal. Mirrors the redwire
617/// frame ceiling (`MAX_FRAME_SIZE` = 16 MiB) so a single SQL statement
618/// can never embed a JSON literal larger than the wire payload limit.
619/// Wire-side limit lives in `reddb_wire::redwire::MAX_FRAME_SIZE`;
620/// duplicated here as a parser-side guard so the lexer can fail fast
621/// without depending on the wire crate.
622pub const JSON_LITERAL_MAX_BYTES: usize = 16 * 1024 * 1024;
623
624/// RQL Lexer
625pub struct Lexer<'a> {
626    /// Original input text — kept so the JSON sub-mode can slice raw
627    /// bytes between `{` and the matching `}` without re-tokenising.
628    input: &'a str,
629    /// Input characters
630    chars: Peekable<Chars<'a>>,
631    /// Current position
632    line: u32,
633    column: u32,
634    offset: u32,
635    /// Peeked token
636    peeked: Option<Spanned>,
637    /// Put-back buffer for characters we need to "unconsume"
638    putback: Option<(char, Position)>,
639    /// Maximum identifier characters (DoS limit, issue #87).
640    max_identifier_chars: usize,
641}
642
643impl<'a> Lexer<'a> {
644    /// Raw source text. Lets parsers recover a keyword-colliding
645    /// identifier's typed spelling from its token span (keywords are
646    /// matched case-insensitively, so the token alone has lost it).
647    pub(crate) fn source(&self) -> &'a str {
648        self.input
649    }
650
651    /// Create a new lexer for the given input
652    pub fn new(input: &'a str) -> Self {
653        Self::with_limits(input, crate::limits::ParserLimits::default())
654    }
655
656    /// Create a new lexer with explicit DoS limits.
657    pub fn with_limits(input: &'a str, limits: crate::limits::ParserLimits) -> Self {
658        Self {
659            input,
660            chars: input.chars().peekable(),
661            line: 1,
662            column: 1,
663            offset: 0,
664            peeked: None,
665            putback: None,
666            max_identifier_chars: limits.max_identifier_chars,
667        }
668    }
669
670    /// Maximum identifier-length cap; queried by `scan_identifier`
671    /// to bail with a structured `LexerError` when an identifier
672    /// would exceed the configured cap.
673    ///
674    /// Exercised only by the embedded unit tests (production code reads the
675    /// `max_identifier_chars` field directly); kept verbatim from the
676    /// pre-extraction server module, where a crate-level `allow(dead_code)`
677    /// masked it. Move-only per #1102.
678    #[allow(dead_code)]
679    pub(crate) fn max_identifier_chars(&self) -> usize {
680        self.max_identifier_chars
681    }
682
683    /// Get current position
684    fn position(&self) -> Position {
685        Position::new(self.line, self.column, self.offset)
686    }
687
688    /// Put a character back into the stream
689    fn unget(&mut self, ch: char, pos: Position) {
690        self.putback = Some((ch, pos));
691    }
692
693    /// Advance and get next character
694    fn advance(&mut self) -> Option<char> {
695        // Check putback buffer first
696        if let Some((ch, pos)) = self.putback.take() {
697            // When we re-consume from putback, update position to after the char
698            self.line = pos.line;
699            self.column = pos.column + 1;
700            self.offset = pos.offset + ch.len_utf8() as u32;
701            return Some(ch);
702        }
703
704        let ch = self.chars.next()?;
705        self.offset += ch.len_utf8() as u32;
706        if ch == '\n' {
707            self.line += 1;
708            self.column = 1;
709        } else {
710            self.column += 1;
711        }
712        Some(ch)
713    }
714
715    /// Peek at next character
716    fn peek(&mut self) -> Option<char> {
717        // Check putback buffer first
718        if let Some((ch, _)) = &self.putback {
719            return Some(*ch);
720        }
721        self.chars.peek().copied()
722    }
723
724    /// Skip whitespace and comments
725    ///
726    /// Superseded by `skip_whitespace_simple`; retained verbatim from the
727    /// pre-extraction server module (where a crate-level `allow(dead_code)`
728    /// masked it). Kept untouched per the move-only contract of #1102.
729    #[allow(dead_code)]
730    fn skip_whitespace(&mut self) {
731        while let Some(ch) = self.peek() {
732            if ch.is_whitespace() {
733                self.advance();
734            } else if ch == '-' {
735                // Could be comment (--) or operator
736                let pos = self.position();
737                self.advance();
738                if self.peek() == Some('-') {
739                    // Line comment
740                    self.advance();
741                    while let Some(c) = self.peek() {
742                        if c == '\n' {
743                            break;
744                        }
745                        self.advance();
746                    }
747                } else {
748                    // Not a comment, put back - by restoring state
749                    // Since we can't put back, we'll handle this in next_token
750                    self.line = pos.line;
751                    self.column = pos.column;
752                    self.offset = pos.offset;
753                    // Need to reset chars iterator - this is tricky
754                    // Instead, we'll handle -- in scan_operator
755                    break;
756                }
757            } else {
758                break;
759            }
760        }
761    }
762
763    /// Peek at the next token without consuming it
764    pub fn peek_token(&mut self) -> Result<&Spanned, LexerError> {
765        if self.peeked.is_none() {
766            self.peeked = Some(self.next_token_internal()?);
767        }
768        Ok(self.peeked.as_ref().unwrap())
769    }
770
771    /// Get the next token
772    pub fn next_token(&mut self) -> Result<Spanned, LexerError> {
773        if let Some(tok) = self.peeked.take() {
774            return Ok(tok);
775        }
776        self.next_token_internal()
777    }
778
779    /// Internal implementation of next_token
780    fn next_token_internal(&mut self) -> Result<Spanned, LexerError> {
781        self.skip_whitespace_simple();
782
783        let start = self.position();
784
785        let ch = match self.peek() {
786            Some(c) => c,
787            None => {
788                return Ok(Spanned::new(Token::Eof, start, start));
789            }
790        };
791
792        // Dispatch based on first character
793        let token = match ch {
794            // String literals
795            '\'' | '"' => self.scan_string()?,
796
797            // Numbers
798            '0'..='9' => self.scan_number()?,
799
800            // Identifiers and keywords
801            'a'..='z' | 'A'..='Z' | '_' => self.scan_identifier()?,
802
803            // Operators and delimiters
804            '=' => {
805                self.advance();
806                // `=>` is the named-argument arrow (e.g. `resolution => 0.5`);
807                // a bare `=` stays the equality operator.
808                if self.peek() == Some('>') {
809                    self.advance();
810                    Token::FatArrow
811                } else {
812                    Token::Eq
813                }
814            }
815            '<' => self.scan_less_than()?,
816            '>' => self.scan_greater_than()?,
817            '!' => {
818                self.advance();
819                if self.peek() == Some('=') {
820                    self.advance();
821                    Token::Ne
822                } else {
823                    return Err(LexerError::new("Expected '=' after '!'", start));
824                }
825            }
826            '+' => {
827                self.advance();
828                Token::Plus
829            }
830            '-' => self.scan_minus()?,
831            '*' => {
832                self.advance();
833                Token::Star
834            }
835            '/' => {
836                self.advance();
837                Token::Slash
838            }
839            '%' => {
840                self.advance();
841                Token::Percent
842            }
843            '(' => {
844                self.advance();
845                Token::LParen
846            }
847            ')' => {
848                self.advance();
849                Token::RParen
850            }
851            '[' => {
852                self.advance();
853                Token::LBracket
854            }
855            ']' => {
856                self.advance();
857                Token::RBracket
858            }
859            '{' => {
860                // JSON sub-mode trigger: if the next non-whitespace char
861                // after `{` is `"`, scan a balanced raw `{...}` and emit
862                // `Token::JsonLiteral`. Otherwise fall through to the
863                // legacy `LBrace` token (Cypher property bag, etc.).
864                // The empty-object case `{}` also takes the JSON path so
865                // bare `VALUES ({})` matches `VALUES ('{}')`.
866                if self.looks_like_json_object_start() {
867                    return self.scan_json_literal(start);
868                }
869                self.advance();
870                Token::LBrace
871            }
872            '}' => {
873                self.advance();
874                Token::RBrace
875            }
876            ',' => {
877                self.advance();
878                Token::Comma
879            }
880            '.' => self.scan_dot()?,
881            ':' => {
882                self.advance();
883                Token::Colon
884            }
885            ';' => {
886                self.advance();
887                Token::Semi
888            }
889            '$' => {
890                self.advance();
891                Token::Dollar
892            }
893            '?' => {
894                self.advance();
895                Token::Question
896            }
897            '|' => {
898                self.advance();
899                if self.peek() == Some('|') {
900                    self.advance();
901                    Token::DoublePipe
902                } else {
903                    Token::Pipe
904                }
905            }
906            _ => {
907                return Err(LexerError::new(
908                    format!("Unexpected character: '{}'", ch),
909                    start,
910                ));
911            }
912        };
913
914        let end = self.position();
915        Ok(Spanned::new(token, start, end))
916    }
917
918    /// Simple whitespace skip (no comment handling to avoid complexity)
919    fn skip_whitespace_simple(&mut self) {
920        while let Some(ch) = self.peek() {
921            if ch.is_whitespace() {
922                self.advance();
923            } else if ch == '-' && self.input[self.offset as usize..].starts_with("--") {
924                self.advance();
925                self.advance();
926                while let Some(c) = self.peek() {
927                    if c == '\n' {
928                        break;
929                    }
930                    self.advance();
931                }
932            } else if ch == '/' && self.input[self.offset as usize..].starts_with("/*") {
933                self.advance();
934                self.advance();
935                while let Some(c) = self.peek() {
936                    self.advance();
937                    if c == '*' && self.peek() == Some('/') {
938                        self.advance();
939                        break;
940                    }
941                }
942            } else {
943                break;
944            }
945        }
946    }
947
948    /// Scan a string literal
949    fn scan_string(&mut self) -> Result<Token, LexerError> {
950        let quote = self.advance().unwrap(); // ' or "
951        let start = self.position();
952        let mut value = String::new();
953
954        loop {
955            match self.peek() {
956                None => {
957                    return Err(LexerError::new("Unterminated string", start));
958                }
959                Some(c) if c == quote => {
960                    self.advance();
961                    // Check for escaped quote ('')
962                    if self.peek() == Some(quote) {
963                        self.advance();
964                        value.push(quote);
965                    } else {
966                        break;
967                    }
968                }
969                Some('\\') => {
970                    self.advance();
971                    match self.peek() {
972                        Some('n') => {
973                            self.advance();
974                            value.push('\n');
975                        }
976                        Some('r') => {
977                            self.advance();
978                            value.push('\r');
979                        }
980                        Some('t') => {
981                            self.advance();
982                            value.push('\t');
983                        }
984                        Some('\\') => {
985                            self.advance();
986                            value.push('\\');
987                        }
988                        Some(c) if c == quote => {
989                            self.advance();
990                            value.push(quote);
991                        }
992                        Some(c) => {
993                            // Unknown escape, keep as-is
994                            value.push('\\');
995                            value.push(c);
996                            self.advance();
997                        }
998                        None => {
999                            return Err(LexerError::new("Unterminated string", start));
1000                        }
1001                    }
1002                }
1003                Some(c) => {
1004                    self.advance();
1005                    value.push(c);
1006                }
1007            }
1008        }
1009
1010        Ok(Token::String(value))
1011    }
1012
1013    /// Scan a number (integer or float)
1014    fn scan_number(&mut self) -> Result<Token, LexerError> {
1015        let mut value = String::new();
1016        let mut is_float = false;
1017
1018        // Integer part
1019        while let Some(ch) = self.peek() {
1020            if ch.is_ascii_digit() {
1021                value.push(ch);
1022                self.advance();
1023            } else {
1024                break;
1025            }
1026        }
1027
1028        // Check for decimal point
1029        if self.peek() == Some('.') {
1030            // Look ahead to distinguish from .. and method calls
1031            let dot_pos = self.position();
1032            self.advance(); // consume the first '.'
1033
1034            if self.peek() == Some('.') {
1035                // It's ".." - put back the first dot using unget
1036                self.unget('.', dot_pos);
1037                // Return integer without the dot
1038            } else if self.peek().map(|c| c.is_ascii_digit()).unwrap_or(false) {
1039                is_float = true;
1040                value.push('.');
1041                while let Some(ch) = self.peek() {
1042                    if ch.is_ascii_digit() {
1043                        value.push(ch);
1044                        self.advance();
1045                    } else {
1046                        break;
1047                    }
1048                }
1049            } else {
1050                // Just a dot after number (like `x.method`), put it back
1051                self.unget('.', dot_pos);
1052            }
1053        }
1054
1055        // Check for exponent
1056        if self.peek() == Some('e') || self.peek() == Some('E') {
1057            is_float = true;
1058            value.push(self.advance().unwrap());
1059
1060            if self.peek() == Some('+') || self.peek() == Some('-') {
1061                value.push(self.advance().unwrap());
1062            }
1063
1064            while let Some(ch) = self.peek() {
1065                if ch.is_ascii_digit() {
1066                    value.push(ch);
1067                    self.advance();
1068                } else {
1069                    break;
1070                }
1071            }
1072        }
1073
1074        if is_float {
1075            match value.parse::<f64>() {
1076                Ok(n) => Ok(Token::Float(n)),
1077                Err(_) => Err(LexerError::new(
1078                    format!("Invalid float: {}", value),
1079                    self.position(),
1080                )),
1081            }
1082        } else {
1083            match value.parse::<i64>() {
1084                Ok(n) => Ok(Token::Integer(n)),
1085                Err(_) => Err(LexerError::new(
1086                    format!("Invalid integer: {}", value),
1087                    self.position(),
1088                )),
1089            }
1090        }
1091    }
1092
1093    /// Scan an identifier or keyword
1094    fn scan_identifier(&mut self) -> Result<Token, LexerError> {
1095        let start_pos = self.position();
1096        let mut value = String::new();
1097        let max = self.max_identifier_chars;
1098
1099        while let Some(ch) = self.peek() {
1100            if ch.is_alphanumeric() || ch == '_' {
1101                if value.chars().count() >= max {
1102                    // Bail before pushing — every additional char is
1103                    // bounded work for the attacker (1 char of input
1104                    // = 1 byte of allocation), so refuse early.
1105                    return Err(LexerError::with_limit(
1106                        format!(
1107                            "identifier exceeds maximum length (max_identifier_chars = {})",
1108                            max
1109                        ),
1110                        start_pos,
1111                        LexerLimitHit::IdentifierTooLong {
1112                            limit_name: "max_identifier_chars",
1113                            value: max,
1114                        },
1115                    ));
1116                }
1117                value.push(ch);
1118                self.advance();
1119            } else {
1120                break;
1121            }
1122        }
1123
1124        // Check for keywords (case-insensitive)
1125        let token = match value.to_uppercase().as_str() {
1126            "SELECT" => Token::Select,
1127            "FROM" => Token::From,
1128            "WHERE" => Token::Where,
1129            "AND" => Token::And,
1130            "OR" => Token::Or,
1131            "NOT" => Token::Not,
1132            "MATCH" => Token::Match,
1133            "RETURN" => Token::Return,
1134            "JOIN" => Token::Join,
1135            "GRAPH" => Token::Graph,
1136            "PATH" => Token::Path,
1137            "TO" => Token::To,
1138            "VIA" => Token::Via,
1139            "ON" => Token::On,
1140            "AS" => Token::As,
1141            "IS" => Token::Is,
1142            "NULL" => Token::Null,
1143            "BETWEEN" => Token::Between,
1144            "LIKE" => Token::Like,
1145            "IN" => Token::In,
1146            "ORDER" => Token::Order,
1147            "BY" => Token::By,
1148            "ASC" => Token::Asc,
1149            "DESC" => Token::Desc,
1150            "NULLS" => Token::Nulls,
1151            "FIRST" => Token::First,
1152            "LAST" => Token::Last,
1153            "LIMIT" => Token::Limit,
1154            "OFFSET" => Token::Offset,
1155            "INNER" => Token::Inner,
1156            "LEFT" => Token::Left,
1157            "RIGHT" => Token::Right,
1158            "OUTER" => Token::Outer,
1159            "FULL" => Token::Full,
1160            "CROSS" => Token::Cross,
1161            "STARTS" => Token::Starts,
1162            "ENDS" => Token::Ends,
1163            "WITH" => Token::With,
1164            "CONTAINS" => Token::Contains,
1165            "TRUE" => Token::True,
1166            "FALSE" => Token::False,
1167            "ENRICH" => Token::Enrich,
1168            "GROUP" => Token::Group,
1169            "COUNT" => Token::Count,
1170            "SUM" => Token::Sum,
1171            "AVG" => Token::Avg,
1172            "MIN" => Token::Min,
1173            "MAX" => Token::Max,
1174            "DISTINCT" => Token::Distinct,
1175            "VECTOR" => Token::Vector,
1176            "SEARCH" => Token::Search,
1177            "SIMILAR" => Token::Similar,
1178            "COLLECTION" => Token::Collection,
1179            "METRIC" => Token::Metric,
1180            "THRESHOLD" => Token::Threshold,
1181            "K" => Token::K,
1182            "HYBRID" => Token::Hybrid,
1183            "FUSION" => Token::Fusion,
1184            "RERANK" => Token::Rerank,
1185            "RRF" => Token::Rrf,
1186            "INTERSECTION" => Token::Intersection,
1187            "UNION" => Token::Union,
1188            "RECURSIVE" => Token::Recursive,
1189            "ALL" => Token::All,
1190            "WEIGHT" => Token::Weight,
1191            "L2" => Token::L2,
1192            "COSINE" => Token::Cosine,
1193            "INNER_PRODUCT" | "INNERPRODUCT" => Token::InnerProduct,
1194            "INCLUDE" => Token::Include,
1195            "METADATA" => Token::Metadata,
1196            "VECTORS" => Token::Vectors,
1197            "EXPLAIN" => Token::Explain,
1198            "FOR" => Token::For,
1199            "FORMAT" => Token::Format,
1200            "JSON" => Token::Json,
1201            "INSERT" => Token::Insert,
1202            "INTO" => Token::Into,
1203            "VALUES" => Token::Values,
1204            "UPDATE" => Token::Update,
1205            "SET" => Token::Set,
1206            "DELETE" => Token::Delete,
1207            "TRUNCATE" => Token::Truncate,
1208            "CREATE" => Token::Create,
1209            "TABLE" => Token::Table,
1210            "DROP" => Token::Drop,
1211            "ALTER" => Token::Alter,
1212            "ADD" => Token::Add,
1213            "COLUMN" => Token::Column,
1214            "PRIMARY" => Token::Primary,
1215            "KEY" => Token::Key,
1216            "DEFAULT" => Token::Default,
1217            "COMPRESS" => Token::Compress,
1218            "INDEX" => Token::Index,
1219            "UNIQUE" => Token::Unique,
1220            "IF" => Token::If,
1221            "EXISTS" => Token::Exists,
1222            "RETURNING" => Token::Returning,
1223            "CASCADE" => Token::Cascade,
1224            "RENAME" => Token::Rename,
1225            "USING" => Token::Using,
1226            "NODE" => Token::Node,
1227            "EDGE" => Token::Edge,
1228            "DOCUMENT" => Token::Document,
1229            "KV" => Token::Kv,
1230            "TIMESERIES" => Token::Timeseries,
1231            "RETENTION" => Token::Retention,
1232            "QUEUE" => Token::Queue,
1233            "TREE" => Token::Tree,
1234            "PUSH" => Token::Push,
1235            "POP" => Token::Pop,
1236            "PEEK" => Token::Peek,
1237            "PURGE" => Token::Purge,
1238            "ACK" => Token::Ack,
1239            "NACK" => Token::Nack,
1240            "PRIORITY" => Token::Priority,
1241            "LPUSH" => Token::Ident("LPUSH".to_string()),
1242            "RPUSH" => Token::Ident("RPUSH".to_string()),
1243            "LPOP" => Token::Ident("LPOP".to_string()),
1244            "RPOP" => Token::Ident("RPOP".to_string()),
1245            "NEIGHBORHOOD" => Token::Neighborhood,
1246            "SHORTEST_PATH" | "SHORTESTPATH" => Token::ShortestPath,
1247            "CENTRALITY" => Token::Centrality,
1248            "COMMUNITY" => Token::Community,
1249            "COMPONENTS" => Token::Components,
1250            "CYCLES" => Token::Cycles,
1251            "TRAVERSE" => Token::Traverse,
1252            "DEPTH" => Token::Depth,
1253            "DIRECTION" => Token::Direction,
1254            "ALGORITHM" => Token::Algorithm,
1255            "STRATEGY" => Token::Strategy,
1256            "MAX_ITERATIONS" | "MAXITERATIONS" => Token::MaxIterations,
1257            "MAX_LENGTH" | "MAXLENGTH" => Token::MaxLength,
1258            "MODE" => Token::Mode,
1259            "CLUSTERING" => Token::Clustering,
1260            "TOPOLOGICAL_SORT" | "TOPOLOGICALSORT" => Token::TopologicalSort,
1261            "PROPERTIES" => Token::Properties,
1262            "TEXT" => Token::Text,
1263            "FUZZY" => Token::Fuzzy,
1264            "MIN_SCORE" | "MINSCORE" => Token::MinScore,
1265            "BEGIN" => Token::Begin,
1266            "COMMIT" => Token::Commit,
1267            "ROLLBACK" => Token::Rollback,
1268            "SAVEPOINT" => Token::Savepoint,
1269            "RELEASE" => Token::Release,
1270            "START" => Token::Start,
1271            "TRANSACTION" => Token::Transaction,
1272            "WORK" => Token::Work,
1273            "VACUUM" => Token::Vacuum,
1274            "ANALYZE" => Token::Analyze,
1275            "SCHEMA" => Token::Schema,
1276            "SEQUENCE" => Token::Sequence,
1277            "INCREMENT" => Token::Increment,
1278            "COPY" => Token::Copy,
1279            "HEADER" => Token::Header,
1280            "DELIMITER" => Token::Delimiter,
1281            "VIEW" => Token::View,
1282            "MATERIALIZED" => Token::Materialized,
1283            "REFRESH" => Token::Refresh,
1284            "PARTITION" => Token::Partition,
1285            "RANGE" => Token::Range,
1286            "LIST" => Token::List,
1287            "HASH" => Token::Hash,
1288            "ATTACH" => Token::Attach,
1289            "DETACH" => Token::Detach,
1290            "OF" => Token::Of,
1291            "POLICY" => Token::Policy,
1292            "ENABLE" => Token::Enable,
1293            "DISABLE" => Token::Disable,
1294            "SECURITY" => Token::Security,
1295            "ROW" => Token::Row,
1296            "LEVEL" => Token::Level,
1297            "FOREIGN" => Token::Foreign,
1298            "SERVER" => Token::Server,
1299            "WRAPPER" => Token::Wrapper,
1300            "OPTIONS" => Token::Options,
1301            "DATA" => Token::Data,
1302            "SESSIONIZE" => Token::Sessionize,
1303            "GAP" => Token::Gap,
1304            "OVER" => Token::Over,
1305            "ROWS" => Token::Rows,
1306            "PRECEDING" => Token::Preceding,
1307            "FOLLOWING" => Token::Following,
1308            "UNBOUNDED" => Token::Unbounded,
1309            "CURRENT" => Token::Current,
1310            _ => Token::Ident(value),
1311        };
1312
1313        Ok(token)
1314    }
1315
1316    /// Scan less-than variants: <, <=, <>, <-
1317    fn scan_less_than(&mut self) -> Result<Token, LexerError> {
1318        self.advance(); // consume '<'
1319        match self.peek() {
1320            Some('=') => {
1321                self.advance();
1322                Ok(Token::Le)
1323            }
1324            Some('>') => {
1325                self.advance();
1326                Ok(Token::Ne)
1327            }
1328            Some('-') => {
1329                self.advance();
1330                Ok(Token::ArrowLeft)
1331            }
1332            _ => Ok(Token::Lt),
1333        }
1334    }
1335
1336    /// Scan greater-than variants: >, >=
1337    fn scan_greater_than(&mut self) -> Result<Token, LexerError> {
1338        self.advance(); // consume '>'
1339        if self.peek() == Some('=') {
1340            self.advance();
1341            Ok(Token::Ge)
1342        } else {
1343            Ok(Token::Gt)
1344        }
1345    }
1346
1347    /// Scan minus variants: -, ->, --comment
1348    fn scan_minus(&mut self) -> Result<Token, LexerError> {
1349        self.advance(); // consume '-'
1350        match self.peek() {
1351            Some('>') => {
1352                self.advance();
1353                Ok(Token::Arrow)
1354            }
1355            Some('-') => {
1356                // Line comment, skip to end of line
1357                self.advance();
1358                while let Some(c) = self.peek() {
1359                    if c == '\n' {
1360                        break;
1361                    }
1362                    self.advance();
1363                }
1364                // Recursively get next token
1365                self.skip_whitespace_simple();
1366                if self.peek().is_none() {
1367                    Ok(Token::Eof)
1368                } else {
1369                    let next = self.next_token_internal()?;
1370                    Ok(next.token)
1371                }
1372            }
1373            _ => Ok(Token::Dash),
1374        }
1375    }
1376
1377    /// Scan dot variants: ., ..
1378    fn scan_dot(&mut self) -> Result<Token, LexerError> {
1379        self.advance(); // consume '.'
1380        if self.peek() == Some('.') {
1381            self.advance();
1382            Ok(Token::DotDot)
1383        } else {
1384            Ok(Token::Dot)
1385        }
1386    }
1387
1388    /// Look ahead from the current `{` to decide whether this is a
1389    /// JSON object literal (next non-ws char is `"` or `}`) or a
1390    /// legacy brace token (Cypher property bag, Python-style key
1391    /// without quotes, etc.). Pure read — does not advance.
1392    fn looks_like_json_object_start(&self) -> bool {
1393        let bytes = self.input.as_bytes();
1394        let mut i = self.offset as usize;
1395        // We're at `{`. Look one past it.
1396        debug_assert!(bytes.get(i) == Some(&b'{'));
1397        i += 1;
1398        while i < bytes.len() {
1399            match bytes[i] {
1400                b' ' | b'\t' | b'\n' | b'\r' => i += 1,
1401                b'"' | b'}' => return true,
1402                _ => return false,
1403            }
1404        }
1405        false
1406    }
1407
1408    /// JSON sub-mode: scan a balanced `{...}` from the current `{`,
1409    /// returning a `Spanned(Token::JsonLiteral(raw_text), …)`.
1410    ///
1411    /// Tracks string boundaries so `{` and `}` inside `"..."` don't
1412    /// affect the brace counter. Honours `\\`, `\"`, `\\` etc. so an
1413    /// escaped quote does not close the string. Errors on EOF inside
1414    /// an unbalanced literal and on payload size > JSON_LITERAL_MAX_BYTES.
1415    ///
1416    /// State machine:
1417    /// - `Outside` (default): counts `{`/`}`, transitions to `InString` on `"`
1418    /// - `InString`: ignores braces, transitions back to `Outside` on
1419    ///   unescaped `"`. On `\`, transitions to `EscapeInString`.
1420    /// - `EscapeInString`: consume one byte unconditionally then back to
1421    ///   `InString`. Multi-byte UTF-8 sequences after `\u` are handled by
1422    ///   ordinary char iteration; we don't validate the JSON here, just
1423    ///   the brace balance.
1424    fn scan_json_literal(&mut self, start: Position) -> Result<Spanned, LexerError> {
1425        let start_offset = self.offset as usize;
1426        // Consume the opening `{`.
1427        self.advance();
1428        let mut depth: u32 = 1;
1429        let mut in_string = false;
1430        let mut escape = false;
1431        loop {
1432            let ch = match self.peek() {
1433                Some(c) => c,
1434                None => {
1435                    return Err(LexerError::new(
1436                        format!(
1437                            "unterminated JSON object literal (started at offset {})",
1438                            start.offset
1439                        ),
1440                        self.position(),
1441                    ));
1442                }
1443            };
1444
1445            // Enforce payload size limit on the raw scan.
1446            let scanned_bytes = self.offset as usize - start_offset;
1447            if scanned_bytes > JSON_LITERAL_MAX_BYTES {
1448                return Err(LexerError::new(
1449                    format!(
1450                        "JSON object literal exceeds JSON_LITERAL_MAX_BYTES ({} bytes)",
1451                        JSON_LITERAL_MAX_BYTES
1452                    ),
1453                    start,
1454                ));
1455            }
1456
1457            self.advance();
1458
1459            if escape {
1460                escape = false;
1461                continue;
1462            }
1463
1464            if in_string {
1465                match ch {
1466                    '\\' => escape = true,
1467                    '"' => in_string = false,
1468                    _ => {}
1469                }
1470                continue;
1471            }
1472
1473            match ch {
1474                '"' => in_string = true,
1475                '{' => depth += 1,
1476                '}' => {
1477                    depth -= 1;
1478                    if depth == 0 {
1479                        let end = self.position();
1480                        let end_offset = self.offset as usize;
1481                        // Final size check including the trailing `}`.
1482                        if end_offset - start_offset > JSON_LITERAL_MAX_BYTES {
1483                            return Err(LexerError::new(
1484                                format!(
1485                                    "JSON object literal exceeds JSON_LITERAL_MAX_BYTES ({} bytes)",
1486                                    JSON_LITERAL_MAX_BYTES
1487                                ),
1488                                start,
1489                            ));
1490                        }
1491                        let raw = self.input[start_offset..end_offset].to_string();
1492                        return Ok(Spanned::new(Token::JsonLiteral(raw), start, end));
1493                    }
1494                }
1495                _ => {}
1496            }
1497        }
1498    }
1499
1500    /// Tokenize entire input
1501    pub fn tokenize(&mut self) -> Result<Vec<Spanned>, LexerError> {
1502        let mut tokens = Vec::new();
1503        loop {
1504            let tok = self.next_token()?;
1505            let is_eof = tok.token == Token::Eof;
1506            tokens.push(tok);
1507            if is_eof {
1508                break;
1509            }
1510        }
1511        Ok(tokens)
1512    }
1513}
1514
1515// ============================================================================
1516// Tests
1517// ============================================================================
1518
1519#[cfg(test)]
1520mod tests {
1521    use super::*;
1522
1523    fn tokenize(input: &str) -> Vec<Token> {
1524        let mut lexer = Lexer::new(input);
1525        lexer
1526            .tokenize()
1527            .unwrap()
1528            .into_iter()
1529            .map(|s| s.token)
1530            .collect()
1531    }
1532
1533    #[test]
1534    fn test_keywords() {
1535        let tokens = tokenize("SELECT FROM WHERE AND OR NOT");
1536        assert_eq!(
1537            tokens,
1538            vec![
1539                Token::Select,
1540                Token::From,
1541                Token::Where,
1542                Token::And,
1543                Token::Or,
1544                Token::Not,
1545                Token::Eof
1546            ]
1547        );
1548    }
1549
1550    #[test]
1551    fn test_identifiers() {
1552        let tokens = tokenize("hosts users ip_address");
1553        assert_eq!(
1554            tokens,
1555            vec![
1556                Token::Ident("hosts".into()),
1557                Token::Ident("users".into()),
1558                Token::Ident("ip_address".into()),
1559                Token::Eof
1560            ]
1561        );
1562    }
1563
1564    #[test]
1565    fn test_numbers() {
1566        let tokens = tokenize("42 2.5 1e10 2.5e-3");
1567        assert_eq!(
1568            tokens,
1569            vec![
1570                Token::Integer(42),
1571                Token::Float(2.5),
1572                Token::Float(1e10),
1573                Token::Float(2.5e-3),
1574                Token::Eof
1575            ]
1576        );
1577    }
1578
1579    #[test]
1580    fn test_strings() {
1581        let tokens = tokenize("'hello' \"world\" 'it''s'");
1582        assert_eq!(
1583            tokens,
1584            vec![
1585                Token::String("hello".into()),
1586                Token::String("world".into()),
1587                Token::String("it's".into()),
1588                Token::Eof
1589            ]
1590        );
1591    }
1592
1593    #[test]
1594    fn test_operators() {
1595        let tokens = tokenize("= <> < <= > >= != + - * /");
1596        assert_eq!(
1597            tokens,
1598            vec![
1599                Token::Eq,
1600                Token::Ne,
1601                Token::Lt,
1602                Token::Le,
1603                Token::Gt,
1604                Token::Ge,
1605                Token::Ne,
1606                Token::Plus,
1607                Token::Dash,
1608                Token::Star,
1609                Token::Slash,
1610                Token::Eof
1611            ]
1612        );
1613    }
1614
1615    #[test]
1616    fn test_delimiters() {
1617        // Note: `{ a }` (not `{ }`) — a bare `{ }` now triggers JSON
1618        // sub-mode and lexes as a single `JsonLiteral("{ }")` token.
1619        // The brace pair around a non-string token still produces the
1620        // legacy LBrace/RBrace pair (Cypher property bag, etc.).
1621        let tokens = tokenize("( ) [ ] { a } , . : ;");
1622        assert_eq!(
1623            tokens,
1624            vec![
1625                Token::LParen,
1626                Token::RParen,
1627                Token::LBracket,
1628                Token::RBracket,
1629                Token::LBrace,
1630                Token::Ident("a".into()),
1631                Token::RBrace,
1632                Token::Comma,
1633                Token::Dot,
1634                Token::Colon,
1635                Token::Semi,
1636                Token::Eof
1637            ]
1638        );
1639    }
1640
1641    #[test]
1642    fn test_json_literal_empty_object() {
1643        let tokens = tokenize("{ }");
1644        assert_eq!(tokens, vec![Token::JsonLiteral("{ }".into()), Token::Eof]);
1645    }
1646
1647    #[test]
1648    fn test_json_literal_simple() {
1649        let tokens = tokenize(r#"{"a":1}"#);
1650        assert_eq!(
1651            tokens,
1652            vec![Token::JsonLiteral(r#"{"a":1}"#.into()), Token::Eof]
1653        );
1654    }
1655
1656    #[test]
1657    fn test_json_literal_nested() {
1658        let raw = r#"{"a":{"b":[1,2,{"c":"}"}]}}"#;
1659        let tokens = tokenize(raw);
1660        assert_eq!(tokens, vec![Token::JsonLiteral(raw.into()), Token::Eof]);
1661    }
1662
1663    #[test]
1664    fn test_json_literal_escaped_quote_in_string() {
1665        // The `}` inside the escaped-quote string must not close the object.
1666        let raw = r#"{"path":"O\"Brien}"}"#;
1667        let tokens = tokenize(raw);
1668        assert_eq!(tokens, vec![Token::JsonLiteral(raw.into()), Token::Eof]);
1669    }
1670
1671    #[test]
1672    fn test_json_literal_unbalanced_eof() {
1673        let mut lexer = Lexer::new(r#"{"a":1"#);
1674        let err = lexer.tokenize().expect_err("expected unterminated error");
1675        assert!(
1676            err.message.contains("unterminated JSON object literal"),
1677            "got: {}",
1678            err.message
1679        );
1680    }
1681
1682    #[test]
1683    fn test_json_literal_property_bag_compatible() {
1684        // Cypher-style property bag must still tokenise as LBrace/.../RBrace
1685        // because the inner content does not start with `"`.
1686        let tokens = tokenize("{name: 'value'}");
1687        assert_eq!(tokens[0], Token::LBrace);
1688        assert_eq!(*tokens.last().unwrap(), Token::Eof);
1689    }
1690
1691    #[test]
1692    fn test_graph_syntax() {
1693        let tokens = tokenize("-> <- - ..");
1694        assert_eq!(
1695            tokens,
1696            vec![
1697                Token::Arrow,
1698                Token::ArrowLeft,
1699                Token::Dash,
1700                Token::DotDot,
1701                Token::Eof
1702            ]
1703        );
1704    }
1705
1706    #[test]
1707    fn test_table_query() {
1708        let tokens = tokenize("SELECT ip, hostname FROM hosts WHERE os = 'Linux' LIMIT 10");
1709        assert_eq!(
1710            tokens,
1711            vec![
1712                Token::Select,
1713                Token::Ident("ip".into()),
1714                Token::Comma,
1715                Token::Ident("hostname".into()),
1716                Token::From,
1717                Token::Ident("hosts".into()),
1718                Token::Where,
1719                Token::Ident("os".into()),
1720                Token::Eq,
1721                Token::String("Linux".into()),
1722                Token::Limit,
1723                Token::Integer(10),
1724                Token::Eof
1725            ]
1726        );
1727    }
1728
1729    #[test]
1730    fn test_graph_query() {
1731        let tokens = tokenize("MATCH (h:Host)-[:HAS_SERVICE]->(s:Service) RETURN h, s");
1732        assert_eq!(
1733            tokens,
1734            vec![
1735                Token::Match,
1736                Token::LParen,
1737                Token::Ident("h".into()),
1738                Token::Colon,
1739                Token::Ident("Host".into()),
1740                Token::RParen,
1741                Token::Dash,
1742                Token::LBracket,
1743                Token::Colon,
1744                Token::Ident("HAS_SERVICE".into()),
1745                Token::RBracket,
1746                Token::Arrow,
1747                Token::LParen,
1748                Token::Ident("s".into()),
1749                Token::Colon,
1750                Token::Ident("Service".into()),
1751                Token::RParen,
1752                Token::Return,
1753                Token::Ident("h".into()),
1754                Token::Comma,
1755                Token::Ident("s".into()),
1756                Token::Eof
1757            ]
1758        );
1759    }
1760
1761    #[test]
1762    fn test_join_query() {
1763        let tokens = tokenize("FROM hosts h JOIN GRAPH (h)-[:HAS_VULN]->(v) ON h.ip = v.id");
1764        assert_eq!(
1765            tokens,
1766            vec![
1767                Token::From,
1768                Token::Ident("hosts".into()),
1769                Token::Ident("h".into()),
1770                Token::Join,
1771                Token::Graph,
1772                Token::LParen,
1773                Token::Ident("h".into()),
1774                Token::RParen,
1775                Token::Dash,
1776                Token::LBracket,
1777                Token::Colon,
1778                Token::Ident("HAS_VULN".into()),
1779                Token::RBracket,
1780                Token::Arrow,
1781                Token::LParen,
1782                Token::Ident("v".into()),
1783                Token::RParen,
1784                Token::On,
1785                Token::Ident("h".into()),
1786                Token::Dot,
1787                Token::Ident("ip".into()),
1788                Token::Eq,
1789                Token::Ident("v".into()),
1790                Token::Dot,
1791                Token::Ident("id".into()),
1792                Token::Eof
1793            ]
1794        );
1795    }
1796
1797    #[test]
1798    fn test_path_query() {
1799        let tokens = tokenize("PATH FROM host('192.168.1.1') TO host('10.0.0.1') VIA [:AUTH]");
1800        assert_eq!(
1801            tokens,
1802            vec![
1803                Token::Path,
1804                Token::From,
1805                Token::Ident("host".into()),
1806                Token::LParen,
1807                Token::String("192.168.1.1".into()),
1808                Token::RParen,
1809                Token::To,
1810                Token::Ident("host".into()),
1811                Token::LParen,
1812                Token::String("10.0.0.1".into()),
1813                Token::RParen,
1814                Token::Via,
1815                Token::LBracket,
1816                Token::Colon,
1817                Token::Ident("AUTH".into()),
1818                Token::RBracket,
1819                Token::Eof
1820            ]
1821        );
1822    }
1823
1824    #[test]
1825    fn test_variable_length_pattern() {
1826        let tokens = tokenize("(a)-[*1..5]->(b)");
1827        assert_eq!(
1828            tokens,
1829            vec![
1830                Token::LParen,
1831                Token::Ident("a".into()),
1832                Token::RParen,
1833                Token::Dash,
1834                Token::LBracket,
1835                Token::Star,
1836                Token::Integer(1),
1837                Token::DotDot,
1838                Token::Integer(5),
1839                Token::RBracket,
1840                Token::Arrow,
1841                Token::LParen,
1842                Token::Ident("b".into()),
1843                Token::RParen,
1844                Token::Eof
1845            ]
1846        );
1847    }
1848
1849    #[test]
1850    fn test_case_insensitive_keywords() {
1851        let tokens = tokenize("select FROM Where AND");
1852        assert_eq!(
1853            tokens,
1854            vec![
1855                Token::Select,
1856                Token::From,
1857                Token::Where,
1858                Token::And,
1859                Token::Eof
1860            ]
1861        );
1862    }
1863
1864    #[test]
1865    fn test_comments() {
1866        let tokens = tokenize("SELECT -- this is a comment\nip FROM hosts");
1867        assert_eq!(
1868            tokens,
1869            vec![
1870                Token::Select,
1871                Token::Ident("ip".into()),
1872                Token::From,
1873                Token::Ident("hosts".into()),
1874                Token::Eof
1875            ]
1876        );
1877    }
1878
1879    #[test]
1880    fn test_escaped_strings() {
1881        let tokens = tokenize(r"'hello\nworld' 'tab\there'");
1882        assert_eq!(
1883            tokens,
1884            vec![
1885                Token::String("hello\nworld".into()),
1886                Token::String("tab\there".into()),
1887                Token::Eof
1888            ]
1889        );
1890    }
1891
1892    #[test]
1893    fn test_keyword_matrix_and_alias_spellings() {
1894        let cases = [
1895            ("SELECT", Token::Select),
1896            ("FROM", Token::From),
1897            ("WHERE", Token::Where),
1898            ("AND", Token::And),
1899            ("OR", Token::Or),
1900            ("NOT", Token::Not),
1901            ("MATCH", Token::Match),
1902            ("RETURN", Token::Return),
1903            ("JOIN", Token::Join),
1904            ("GRAPH", Token::Graph),
1905            ("PATH", Token::Path),
1906            ("TO", Token::To),
1907            ("VIA", Token::Via),
1908            ("ON", Token::On),
1909            ("AS", Token::As),
1910            ("IS", Token::Is),
1911            ("NULL", Token::Null),
1912            ("BETWEEN", Token::Between),
1913            ("LIKE", Token::Like),
1914            ("IN", Token::In),
1915            ("ORDER", Token::Order),
1916            ("BY", Token::By),
1917            ("ASC", Token::Asc),
1918            ("DESC", Token::Desc),
1919            ("NULLS", Token::Nulls),
1920            ("FIRST", Token::First),
1921            ("LAST", Token::Last),
1922            ("LIMIT", Token::Limit),
1923            ("OFFSET", Token::Offset),
1924            ("INNER", Token::Inner),
1925            ("LEFT", Token::Left),
1926            ("RIGHT", Token::Right),
1927            ("OUTER", Token::Outer),
1928            ("FULL", Token::Full),
1929            ("CROSS", Token::Cross),
1930            ("STARTS", Token::Starts),
1931            ("ENDS", Token::Ends),
1932            ("WITH", Token::With),
1933            ("CONTAINS", Token::Contains),
1934            ("TRUE", Token::True),
1935            ("FALSE", Token::False),
1936            ("ENRICH", Token::Enrich),
1937            ("GROUP", Token::Group),
1938            ("COUNT", Token::Count),
1939            ("SUM", Token::Sum),
1940            ("AVG", Token::Avg),
1941            ("MIN", Token::Min),
1942            ("MAX", Token::Max),
1943            ("DISTINCT", Token::Distinct),
1944            ("VECTOR", Token::Vector),
1945            ("SEARCH", Token::Search),
1946            ("SIMILAR", Token::Similar),
1947            ("COLLECTION", Token::Collection),
1948            ("METRIC", Token::Metric),
1949            ("THRESHOLD", Token::Threshold),
1950            ("K", Token::K),
1951            ("HYBRID", Token::Hybrid),
1952            ("FUSION", Token::Fusion),
1953            ("RERANK", Token::Rerank),
1954            ("RRF", Token::Rrf),
1955            ("INTERSECTION", Token::Intersection),
1956            ("UNION", Token::Union),
1957            ("RECURSIVE", Token::Recursive),
1958            ("ALL", Token::All),
1959            ("WEIGHT", Token::Weight),
1960            ("L2", Token::L2),
1961            ("COSINE", Token::Cosine),
1962            ("INNER_PRODUCT", Token::InnerProduct),
1963            ("INNERPRODUCT", Token::InnerProduct),
1964            ("INCLUDE", Token::Include),
1965            ("METADATA", Token::Metadata),
1966            ("VECTORS", Token::Vectors),
1967            ("EXPLAIN", Token::Explain),
1968            ("FOR", Token::For),
1969            ("FORMAT", Token::Format),
1970            ("JSON", Token::Json),
1971            ("INSERT", Token::Insert),
1972            ("INTO", Token::Into),
1973            ("VALUES", Token::Values),
1974            ("UPDATE", Token::Update),
1975            ("SET", Token::Set),
1976            ("DELETE", Token::Delete),
1977            ("TRUNCATE", Token::Truncate),
1978            ("CREATE", Token::Create),
1979            ("TABLE", Token::Table),
1980            ("DROP", Token::Drop),
1981            ("ALTER", Token::Alter),
1982            ("ADD", Token::Add),
1983            ("COLUMN", Token::Column),
1984            ("PRIMARY", Token::Primary),
1985            ("KEY", Token::Key),
1986            ("DEFAULT", Token::Default),
1987            ("COMPRESS", Token::Compress),
1988            ("INDEX", Token::Index),
1989            ("UNIQUE", Token::Unique),
1990            ("IF", Token::If),
1991            ("EXISTS", Token::Exists),
1992            ("RETURNING", Token::Returning),
1993            ("CASCADE", Token::Cascade),
1994            ("RENAME", Token::Rename),
1995            ("USING", Token::Using),
1996            ("NODE", Token::Node),
1997            ("EDGE", Token::Edge),
1998            ("DOCUMENT", Token::Document),
1999            ("KV", Token::Kv),
2000            ("TIMESERIES", Token::Timeseries),
2001            ("RETENTION", Token::Retention),
2002            ("QUEUE", Token::Queue),
2003            ("TREE", Token::Tree),
2004            ("PUSH", Token::Push),
2005            ("POP", Token::Pop),
2006            ("PEEK", Token::Peek),
2007            ("PURGE", Token::Purge),
2008            ("ACK", Token::Ack),
2009            ("NACK", Token::Nack),
2010            ("PRIORITY", Token::Priority),
2011            ("LPUSH", Token::Ident("LPUSH".into())),
2012            ("RPUSH", Token::Ident("RPUSH".into())),
2013            ("LPOP", Token::Ident("LPOP".into())),
2014            ("RPOP", Token::Ident("RPOP".into())),
2015            ("NEIGHBORHOOD", Token::Neighborhood),
2016            ("SHORTEST_PATH", Token::ShortestPath),
2017            ("SHORTESTPATH", Token::ShortestPath),
2018            ("CENTRALITY", Token::Centrality),
2019            ("COMMUNITY", Token::Community),
2020            ("COMPONENTS", Token::Components),
2021            ("CYCLES", Token::Cycles),
2022            ("TRAVERSE", Token::Traverse),
2023            ("DEPTH", Token::Depth),
2024            ("DIRECTION", Token::Direction),
2025            ("ALGORITHM", Token::Algorithm),
2026            ("STRATEGY", Token::Strategy),
2027            ("MAX_ITERATIONS", Token::MaxIterations),
2028            ("MAXITERATIONS", Token::MaxIterations),
2029            ("MAX_LENGTH", Token::MaxLength),
2030            ("MAXLENGTH", Token::MaxLength),
2031            ("MODE", Token::Mode),
2032            ("CLUSTERING", Token::Clustering),
2033            ("TOPOLOGICAL_SORT", Token::TopologicalSort),
2034            ("TOPOLOGICALSORT", Token::TopologicalSort),
2035            ("PROPERTIES", Token::Properties),
2036            ("TEXT", Token::Text),
2037            ("FUZZY", Token::Fuzzy),
2038            ("MIN_SCORE", Token::MinScore),
2039            ("MINSCORE", Token::MinScore),
2040            ("BEGIN", Token::Begin),
2041            ("COMMIT", Token::Commit),
2042            ("ROLLBACK", Token::Rollback),
2043            ("SAVEPOINT", Token::Savepoint),
2044            ("RELEASE", Token::Release),
2045            ("START", Token::Start),
2046            ("TRANSACTION", Token::Transaction),
2047            ("WORK", Token::Work),
2048            ("VACUUM", Token::Vacuum),
2049            ("ANALYZE", Token::Analyze),
2050            ("SCHEMA", Token::Schema),
2051            ("SEQUENCE", Token::Sequence),
2052            ("INCREMENT", Token::Increment),
2053            ("COPY", Token::Copy),
2054            ("HEADER", Token::Header),
2055            ("DELIMITER", Token::Delimiter),
2056            ("VIEW", Token::View),
2057            ("MATERIALIZED", Token::Materialized),
2058            ("REFRESH", Token::Refresh),
2059            ("PARTITION", Token::Partition),
2060            ("RANGE", Token::Range),
2061            ("LIST", Token::List),
2062            ("HASH", Token::Hash),
2063            ("ATTACH", Token::Attach),
2064            ("DETACH", Token::Detach),
2065            ("OF", Token::Of),
2066            ("POLICY", Token::Policy),
2067            ("ENABLE", Token::Enable),
2068            ("DISABLE", Token::Disable),
2069            ("SECURITY", Token::Security),
2070            ("ROW", Token::Row),
2071            ("LEVEL", Token::Level),
2072            ("FOREIGN", Token::Foreign),
2073            ("SERVER", Token::Server),
2074            ("WRAPPER", Token::Wrapper),
2075            ("OPTIONS", Token::Options),
2076            ("DATA", Token::Data),
2077            ("plain_ident", Token::Ident("plain_ident".into())),
2078        ];
2079
2080        for (input, expected) in cases {
2081            let tokens = tokenize(input);
2082            assert_eq!(tokens, vec![expected, Token::Eof], "{input}");
2083        }
2084    }
2085
2086    #[test]
2087    fn test_display_all_token_variants() {
2088        let cases = [
2089            (Token::Select, "SELECT"),
2090            (Token::From, "FROM"),
2091            (Token::Where, "WHERE"),
2092            (Token::And, "AND"),
2093            (Token::Or, "OR"),
2094            (Token::Not, "NOT"),
2095            (Token::Match, "MATCH"),
2096            (Token::Return, "RETURN"),
2097            (Token::Join, "JOIN"),
2098            (Token::Graph, "GRAPH"),
2099            (Token::Path, "PATH"),
2100            (Token::To, "TO"),
2101            (Token::Via, "VIA"),
2102            (Token::On, "ON"),
2103            (Token::As, "AS"),
2104            (Token::Is, "IS"),
2105            (Token::Null, "NULL"),
2106            (Token::Between, "BETWEEN"),
2107            (Token::Like, "LIKE"),
2108            (Token::In, "IN"),
2109            (Token::Order, "ORDER"),
2110            (Token::By, "BY"),
2111            (Token::Asc, "ASC"),
2112            (Token::Desc, "DESC"),
2113            (Token::Nulls, "NULLS"),
2114            (Token::First, "FIRST"),
2115            (Token::Last, "LAST"),
2116            (Token::Limit, "LIMIT"),
2117            (Token::Offset, "OFFSET"),
2118            (Token::Inner, "INNER"),
2119            (Token::Left, "LEFT"),
2120            (Token::Right, "RIGHT"),
2121            (Token::Outer, "OUTER"),
2122            (Token::Full, "FULL"),
2123            (Token::Cross, "CROSS"),
2124            (Token::Starts, "STARTS"),
2125            (Token::Ends, "ENDS"),
2126            (Token::With, "WITH"),
2127            (Token::Contains, "CONTAINS"),
2128            (Token::True, "TRUE"),
2129            (Token::False, "FALSE"),
2130            (Token::Enrich, "ENRICH"),
2131            (Token::Group, "GROUP"),
2132            (Token::Count, "COUNT"),
2133            (Token::Sum, "SUM"),
2134            (Token::Avg, "AVG"),
2135            (Token::Min, "MIN"),
2136            (Token::Max, "MAX"),
2137            (Token::Distinct, "DISTINCT"),
2138            (Token::Vector, "VECTOR"),
2139            (Token::Search, "SEARCH"),
2140            (Token::Similar, "SIMILAR"),
2141            (Token::Collection, "COLLECTION"),
2142            (Token::Metric, "METRIC"),
2143            (Token::Threshold, "THRESHOLD"),
2144            (Token::K, "K"),
2145            (Token::Hybrid, "HYBRID"),
2146            (Token::Fusion, "FUSION"),
2147            (Token::Rerank, "RERANK"),
2148            (Token::Rrf, "RRF"),
2149            (Token::Intersection, "INTERSECTION"),
2150            (Token::Union, "UNION"),
2151            (Token::Recursive, "RECURSIVE"),
2152            (Token::All, "ALL"),
2153            (Token::Weight, "WEIGHT"),
2154            (Token::L2, "L2"),
2155            (Token::Cosine, "COSINE"),
2156            (Token::InnerProduct, "INNER_PRODUCT"),
2157            (Token::Include, "INCLUDE"),
2158            (Token::Metadata, "METADATA"),
2159            (Token::Vectors, "VECTORS"),
2160            (Token::Explain, "EXPLAIN"),
2161            (Token::For, "FOR"),
2162            (Token::Format, "FORMAT"),
2163            (Token::Json, "JSON"),
2164            (Token::Insert, "INSERT"),
2165            (Token::Into, "INTO"),
2166            (Token::Values, "VALUES"),
2167            (Token::Update, "UPDATE"),
2168            (Token::Set, "SET"),
2169            (Token::Delete, "DELETE"),
2170            (Token::Truncate, "TRUNCATE"),
2171            (Token::Create, "CREATE"),
2172            (Token::Table, "TABLE"),
2173            (Token::Drop, "DROP"),
2174            (Token::Alter, "ALTER"),
2175            (Token::Add, "ADD"),
2176            (Token::Column, "COLUMN"),
2177            (Token::Primary, "PRIMARY"),
2178            (Token::Key, "KEY"),
2179            (Token::Default, "DEFAULT"),
2180            (Token::Compress, "COMPRESS"),
2181            (Token::Index, "INDEX"),
2182            (Token::Unique, "UNIQUE"),
2183            (Token::If, "IF"),
2184            (Token::Exists, "EXISTS"),
2185            (Token::Returning, "RETURNING"),
2186            (Token::Cascade, "CASCADE"),
2187            (Token::Rename, "RENAME"),
2188            (Token::Using, "USING"),
2189            (Token::Node, "NODE"),
2190            (Token::Edge, "EDGE"),
2191            (Token::Document, "DOCUMENT"),
2192            (Token::Kv, "KV"),
2193            (Token::Timeseries, "TIMESERIES"),
2194            (Token::Retention, "RETENTION"),
2195            (Token::Queue, "QUEUE"),
2196            (Token::Tree, "TREE"),
2197            (Token::Push, "PUSH"),
2198            (Token::Pop, "POP"),
2199            (Token::Peek, "PEEK"),
2200            (Token::Purge, "PURGE"),
2201            (Token::Ack, "ACK"),
2202            (Token::Nack, "NACK"),
2203            (Token::Priority, "PRIORITY"),
2204            (Token::Neighborhood, "NEIGHBORHOOD"),
2205            (Token::ShortestPath, "SHORTEST_PATH"),
2206            (Token::Centrality, "CENTRALITY"),
2207            (Token::Community, "COMMUNITY"),
2208            (Token::Components, "COMPONENTS"),
2209            (Token::Cycles, "CYCLES"),
2210            (Token::Traverse, "TRAVERSE"),
2211            (Token::Depth, "DEPTH"),
2212            (Token::Direction, "DIRECTION"),
2213            (Token::Algorithm, "ALGORITHM"),
2214            (Token::Strategy, "STRATEGY"),
2215            (Token::MaxIterations, "MAX_ITERATIONS"),
2216            (Token::MaxLength, "MAX_LENGTH"),
2217            (Token::Mode, "MODE"),
2218            (Token::Clustering, "CLUSTERING"),
2219            (Token::TopologicalSort, "TOPOLOGICAL_SORT"),
2220            (Token::Properties, "PROPERTIES"),
2221            (Token::Text, "TEXT"),
2222            (Token::Fuzzy, "FUZZY"),
2223            (Token::MinScore, "MIN_SCORE"),
2224            (Token::Begin, "BEGIN"),
2225            (Token::Commit, "COMMIT"),
2226            (Token::Rollback, "ROLLBACK"),
2227            (Token::Savepoint, "SAVEPOINT"),
2228            (Token::Release, "RELEASE"),
2229            (Token::Start, "START"),
2230            (Token::Transaction, "TRANSACTION"),
2231            (Token::Work, "WORK"),
2232            (Token::Vacuum, "VACUUM"),
2233            (Token::Analyze, "ANALYZE"),
2234            (Token::Schema, "SCHEMA"),
2235            (Token::Sequence, "SEQUENCE"),
2236            (Token::Increment, "INCREMENT"),
2237            (Token::Copy, "COPY"),
2238            (Token::Header, "HEADER"),
2239            (Token::Delimiter, "DELIMITER"),
2240            (Token::View, "VIEW"),
2241            (Token::Materialized, "MATERIALIZED"),
2242            (Token::Refresh, "REFRESH"),
2243            (Token::Partition, "PARTITION"),
2244            (Token::Range, "RANGE"),
2245            (Token::List, "LIST"),
2246            (Token::Hash, "HASH"),
2247            (Token::Attach, "ATTACH"),
2248            (Token::Detach, "DETACH"),
2249            (Token::Of, "OF"),
2250            (Token::Policy, "POLICY"),
2251            (Token::Enable, "ENABLE"),
2252            (Token::Disable, "DISABLE"),
2253            (Token::Security, "SECURITY"),
2254            (Token::Row, "ROW"),
2255            (Token::Level, "LEVEL"),
2256            (Token::Foreign, "FOREIGN"),
2257            (Token::Server, "SERVER"),
2258            (Token::Wrapper, "WRAPPER"),
2259            (Token::Options, "OPTIONS"),
2260            (Token::Data, "DATA"),
2261            (Token::String("x".into()), "'x'"),
2262            (Token::Integer(7), "7"),
2263            (Token::Float(1.5), "1.5"),
2264            (Token::JsonLiteral(r#"{"x":1}"#.into()), r#"{"x":1}"#),
2265            (Token::Ident("id".into()), "id"),
2266            (Token::Eq, "="),
2267            (Token::Ne, "<>"),
2268            (Token::Lt, "<"),
2269            (Token::Le, "<="),
2270            (Token::Gt, ">"),
2271            (Token::Ge, ">="),
2272            (Token::Plus, "+"),
2273            (Token::Minus, "-"),
2274            (Token::Star, "*"),
2275            (Token::Slash, "/"),
2276            (Token::Percent, "%"),
2277            (Token::LParen, "("),
2278            (Token::RParen, ")"),
2279            (Token::LBracket, "["),
2280            (Token::RBracket, "]"),
2281            (Token::LBrace, "{"),
2282            (Token::RBrace, "}"),
2283            (Token::Comma, ","),
2284            (Token::Dot, "."),
2285            (Token::Colon, ":"),
2286            (Token::Semi, ";"),
2287            (Token::Dollar, "$"),
2288            (Token::FatArrow, "=>"),
2289            (Token::Arrow, "->"),
2290            (Token::ArrowLeft, "<-"),
2291            (Token::Dash, "-"),
2292            (Token::DotDot, ".."),
2293            (Token::Pipe, "|"),
2294            (Token::DoublePipe, "||"),
2295            (Token::Eof, "EOF"),
2296        ];
2297
2298        for (token, expected) in cases {
2299            assert_eq!(token.to_string(), expected);
2300        }
2301    }
2302
2303    #[test]
2304    fn fat_arrow_lexes_distinctly_from_eq() {
2305        // `=>` is the named-argument arrow; a bare `=` stays equality.
2306        assert_eq!(
2307            tokenize("resolution => 0.5"),
2308            vec![
2309                Token::Ident("resolution".into()),
2310                Token::FatArrow,
2311                Token::Float(0.5),
2312                Token::Eof,
2313            ]
2314        );
2315        assert_eq!(
2316            tokenize("x = 1"),
2317            vec![
2318                Token::Ident("x".into()),
2319                Token::Eq,
2320                Token::Integer(1),
2321                Token::Eof,
2322            ]
2323        );
2324    }
2325
2326    #[test]
2327    fn test_string_escape_and_error_matrix() {
2328        let tokens = tokenize(
2329            r#"'line\nrow' 'carriage\rreturn' 'tab\tstop' 'slash\\' 'quote\'' "dq\"" 'raw\z'"#,
2330        );
2331        assert_eq!(
2332            tokens,
2333            vec![
2334                Token::String("line\nrow".into()),
2335                Token::String("carriage\rreturn".into()),
2336                Token::String("tab\tstop".into()),
2337                Token::String("slash\\".into()),
2338                Token::String("quote'".into()),
2339                Token::String("dq\"".into()),
2340                Token::String(r"raw\z".into()),
2341                Token::Eof
2342            ]
2343        );
2344
2345        let mut lexer = Lexer::new("'unterminated");
2346        assert!(lexer
2347            .next_token()
2348            .unwrap_err()
2349            .message
2350            .contains("Unterminated string"));
2351
2352        let mut lexer = Lexer::new(r"'bad\");
2353        assert!(lexer
2354            .next_token()
2355            .unwrap_err()
2356            .message
2357            .contains("Unterminated string"));
2358    }
2359
2360    #[test]
2361    fn test_operator_comment_peek_limit_and_tokenize_paths() {
2362        let tokens = tokenize("!= % ; $ || | 123.abc 1..2 1e+2 <- -> /* block */ SELECT");
2363        assert_eq!(
2364            tokens,
2365            vec![
2366                Token::Ne,
2367                Token::Percent,
2368                Token::Semi,
2369                Token::Dollar,
2370                Token::DoublePipe,
2371                Token::Pipe,
2372                Token::Integer(123),
2373                Token::Dot,
2374                Token::Ident("abc".into()),
2375                Token::Integer(1),
2376                Token::DotDot,
2377                Token::Integer(2),
2378                Token::Float(1e2),
2379                Token::ArrowLeft,
2380                Token::Arrow,
2381                Token::Select,
2382                Token::Eof,
2383            ]
2384        );
2385
2386        let mut lexer = Lexer::new("SELECT FROM");
2387        assert_eq!(lexer.peek_token().unwrap().token, Token::Select);
2388        assert_eq!(lexer.next_token().unwrap().token, Token::Select);
2389        assert_eq!(lexer.next_token().unwrap().token, Token::From);
2390
2391        let mut lexer = Lexer::new("!");
2392        assert!(lexer
2393            .next_token()
2394            .unwrap_err()
2395            .message
2396            .contains("Expected '=' after '!'"));
2397
2398        let limits = crate::limits::ParserLimits {
2399            max_identifier_chars: 3,
2400            ..crate::limits::ParserLimits::default()
2401        };
2402        let mut lexer = Lexer::with_limits("abcd", limits);
2403        assert_eq!(lexer.max_identifier_chars(), 3);
2404        let err = lexer.next_token().unwrap_err();
2405        assert!(matches!(
2406            err.limit_hit,
2407            Some(LexerLimitHit::IdentifierTooLong { value: 3, .. })
2408        ));
2409    }
2410}