Skip to main content

polyglot_sql/
tokens.rs

1//! Token types and tokenization for SQL parsing
2//!
3//! This module defines all SQL token types and the tokenizer that converts
4//! SQL strings into token streams.
5
6use crate::error::{Error, Result};
7use crate::guard::TokenGuardStats;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::fmt;
11use std::ops::Deref;
12use std::sync::{Arc, LazyLock};
13#[cfg(feature = "bindings")]
14use ts_rs::TS;
15
16/// Parse a DollarString token text into (tag, content).
17/// If the text contains '\x00', the part before is the tag and after is content.
18/// Otherwise, the whole text is the content with no tag.
19pub fn parse_dollar_string_token(text: &str) -> (Option<String>, String) {
20    if let Some(pos) = text.find('\x00') {
21        let tag = &text[..pos];
22        let content = &text[pos + 1..];
23        (Some(tag.to_string()), content.to_string())
24    } else {
25        (None, text.to_string())
26    }
27}
28
29/// Represents a position in the source SQL
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
31#[cfg_attr(feature = "bindings", derive(TS))]
32pub struct Span {
33    /// Starting Unicode character offset (not a UTF-8 byte or UTF-16 code-unit offset).
34    pub start: usize,
35    /// Ending Unicode character offset (exclusive).
36    pub end: usize,
37    /// Cursor line after consuming the token (1-based).
38    pub line: usize,
39    /// Cursor column after consuming the token (1-based, in Unicode characters).
40    pub column: usize,
41}
42
43impl Span {
44    /// Cover two source ranges, retaining the ending range's cursor location.
45    pub(crate) fn through(self, end: Self) -> Self {
46        Self {
47            start: self.start,
48            ..end
49        }
50    }
51
52    pub fn new(start: usize, end: usize, line: usize, column: usize) -> Self {
53        Self {
54            start,
55            end,
56            line,
57            column,
58        }
59    }
60}
61
62/// A token in the SQL token stream
63#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
64pub struct Token {
65    /// The type of token
66    pub token_type: TokenType,
67    /// The raw text of the token
68    pub text: String,
69    /// Position information
70    pub span: Span,
71    /// Leading comments (comments that appeared before this token)
72    #[serde(default)]
73    pub comments: Vec<String>,
74    /// Trailing comments (comments that appeared after this token, before the next one)
75    #[serde(default)]
76    pub trailing_comments: Vec<String>,
77}
78
79impl Token {
80    /// Create a new token
81    pub fn new(token_type: TokenType, text: impl Into<String>, span: Span) -> Self {
82        Self {
83            token_type,
84            text: text.into(),
85            span,
86            comments: Vec::new(),
87            trailing_comments: Vec::new(),
88        }
89    }
90
91    /// Create a NUMBER token
92    pub fn number(n: i64) -> Self {
93        Self::new(TokenType::Number, n.to_string(), Span::default())
94    }
95
96    /// Create a STRING token
97    pub fn string(s: impl Into<String>) -> Self {
98        Self::new(TokenType::String, s, Span::default())
99    }
100
101    /// Create an IDENTIFIER token
102    pub fn identifier(s: impl Into<String>) -> Self {
103        Self::new(TokenType::Identifier, s, Span::default())
104    }
105
106    /// Create a VAR token
107    pub fn var(s: impl Into<String>) -> Self {
108        Self::new(TokenType::Var, s, Span::default())
109    }
110
111    /// Add a comment to this token
112    pub fn with_comment(mut self, comment: impl Into<String>) -> Self {
113        self.comments.push(comment.into());
114        self
115    }
116}
117
118#[derive(Debug, Clone)]
119pub(crate) enum ParserTokenText {
120    Source {
121        source: Arc<str>,
122        start: usize,
123        end: usize,
124    },
125    Owned(String),
126}
127
128impl Deref for ParserTokenText {
129    type Target = str;
130
131    fn deref(&self) -> &Self::Target {
132        match self {
133            Self::Source { source, start, end } => &source[*start..*end],
134            Self::Owned(text) => text,
135        }
136    }
137}
138
139impl fmt::Display for ParserTokenText {
140    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
141        formatter.write_str(self)
142    }
143}
144
145impl PartialEq<str> for ParserTokenText {
146    fn eq(&self, other: &str) -> bool {
147        self.deref() == other
148    }
149}
150
151impl PartialEq<&str> for ParserTokenText {
152    fn eq(&self, other: &&str) -> bool {
153        self.deref() == *other
154    }
155}
156
157#[derive(Debug, Clone)]
158pub(crate) struct ParserToken {
159    pub token_type: TokenType,
160    pub span: Span,
161    pub comments: Vec<String>,
162    pub trailing_comments: Vec<String>,
163    pub(crate) text: ParserTokenText,
164}
165
166impl ParserToken {
167    pub(crate) fn text(&self) -> &str {
168        &self.text
169    }
170
171    pub(crate) fn text_owned(&self) -> String {
172        self.text.to_string()
173    }
174}
175
176impl From<Token> for ParserToken {
177    fn from(token: Token) -> Self {
178        Self {
179            token_type: token.token_type,
180            span: token.span,
181            comments: token.comments,
182            trailing_comments: token.trailing_comments,
183            text: ParserTokenText::Owned(token.text),
184        }
185    }
186}
187
188trait TokenOutput: Sized {
189    fn from_source(
190        token_type: TokenType,
191        source: &str,
192        text_start: usize,
193        text_end: usize,
194        span: Span,
195        shared_source: Option<&Arc<str>>,
196    ) -> Self;
197    fn from_owned(token_type: TokenType, text: String, span: Span) -> Self;
198    fn token_type(&self) -> TokenType;
199    fn text<'a>(&'a self, source: &'a str) -> &'a str;
200    fn comments_mut(&mut self) -> &mut Vec<String>;
201    fn trailing_comments_mut(&mut self) -> &mut Vec<String>;
202}
203
204impl TokenOutput for Token {
205    fn from_source(
206        token_type: TokenType,
207        source: &str,
208        text_start: usize,
209        text_end: usize,
210        span: Span,
211        _shared_source: Option<&Arc<str>>,
212    ) -> Self {
213        Self::new(token_type, &source[text_start..text_end], span)
214    }
215
216    fn from_owned(token_type: TokenType, text: String, span: Span) -> Self {
217        Self::new(token_type, text, span)
218    }
219
220    fn token_type(&self) -> TokenType {
221        self.token_type
222    }
223
224    fn text<'a>(&'a self, _source: &'a str) -> &'a str {
225        &self.text
226    }
227
228    fn comments_mut(&mut self) -> &mut Vec<String> {
229        &mut self.comments
230    }
231
232    fn trailing_comments_mut(&mut self) -> &mut Vec<String> {
233        &mut self.trailing_comments
234    }
235}
236
237impl TokenOutput for ParserToken {
238    fn from_source(
239        token_type: TokenType,
240        _source: &str,
241        text_start: usize,
242        text_end: usize,
243        span: Span,
244        shared_source: Option<&Arc<str>>,
245    ) -> Self {
246        Self {
247            token_type,
248            span,
249            comments: Vec::new(),
250            trailing_comments: Vec::new(),
251            text: ParserTokenText::Source {
252                source: Arc::clone(shared_source.expect("parser tokenization requires source SQL")),
253                start: text_start,
254                end: text_end,
255            },
256        }
257    }
258
259    fn from_owned(token_type: TokenType, text: String, span: Span) -> Self {
260        Self {
261            token_type,
262            span,
263            comments: Vec::new(),
264            trailing_comments: Vec::new(),
265            text: ParserTokenText::Owned(text),
266        }
267    }
268
269    fn token_type(&self) -> TokenType {
270        self.token_type
271    }
272
273    fn text<'a>(&'a self, _source: &'a str) -> &'a str {
274        self.text()
275    }
276
277    fn comments_mut(&mut self) -> &mut Vec<String> {
278        &mut self.comments
279    }
280
281    fn trailing_comments_mut(&mut self) -> &mut Vec<String> {
282        &mut self.trailing_comments
283    }
284}
285
286impl fmt::Display for Token {
287    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
288        write!(f, "{:?}({})", self.token_type, self.text)
289    }
290}
291
292/// All possible token types in SQL
293#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
294#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
295#[repr(u16)]
296pub enum TokenType {
297    // Punctuation
298    LParen,
299    RParen,
300    LBracket,
301    RBracket,
302    LBrace,
303    RBrace,
304    Comma,
305    Dot,
306    Dash,
307    Plus,
308    Colon,
309    DotColon,
310    DColon,
311    DColonDollar,
312    DColonPercent,
313    DColonQMark,
314    DQMark,
315    Semicolon,
316    Star,
317    Backslash,
318    Slash,
319    Lt,
320    Lte,
321    Gt,
322    Gte,
323    Not,
324    Eq,
325    Neq,
326    NullsafeEq,
327    ColonEq,
328    ColonGt,
329    NColonGt,
330    And,
331    Or,
332    Amp,
333    DPipe,
334    PipeGt,
335    Pipe,
336    PipeSlash,
337    DPipeSlash,
338    Caret,
339    CaretAt,
340    LtLt, // <<
341    GtGt, // >>
342    Tilde,
343    Arrow,
344    DArrow,
345    FArrow,
346    Hash,
347    HashArrow,
348    DHashArrow,
349    LrArrow,
350    DAt,
351    AtAt,
352    AtQMark,
353    LtAt,
354    AtGt,
355    Dollar,
356    Parameter,
357    Session,
358    SessionParameter,
359    SessionUser,
360    DAmp,
361    AmpLt,
362    AmpGt,
363    Adjacent,
364    Xor,
365    DStar,
366    QMarkAmp,
367    QMarkPipe,
368    HashDash,
369    Exclamation,
370
371    UriStart,
372    BlockStart,
373    BlockEnd,
374    Space,
375    Break,
376
377    // Comments (emitted as tokens for round-trip fidelity)
378    BlockComment, // /* ... */
379    LineComment,  // -- ...
380
381    // Literals
382    String,
383    DollarString,             // $$...$$
384    TripleDoubleQuotedString, // """..."""
385    TripleSingleQuotedString, // '''...'''
386    Number,
387    Identifier,
388    QuotedIdentifier,
389    Database,
390    Column,
391    ColumnDef,
392    Schema,
393    Table,
394    Warehouse,
395    Stage,
396    Streamlit,
397    Var,
398    BitString,
399    HexString,
400    /// Hex number: 0xA, 0xFF (BigQuery, SQLite style) - represents an integer in hex notation
401    HexNumber,
402    ByteString,
403    NationalString,
404    EscapeString, // PostgreSQL E'...' escape string
405    RawString,
406    HeredocString,
407    HeredocStringAlternative,
408    UnicodeString,
409
410    // Data Types
411    Bit,
412    Boolean,
413    TinyInt,
414    UTinyInt,
415    SmallInt,
416    USmallInt,
417    MediumInt,
418    UMediumInt,
419    Int,
420    UInt,
421    BigInt,
422    UBigInt,
423    BigNum,
424    Int128,
425    UInt128,
426    Int256,
427    UInt256,
428    Float,
429    Double,
430    UDouble,
431    Decimal,
432    Decimal32,
433    Decimal64,
434    Decimal128,
435    Decimal256,
436    DecFloat,
437    UDecimal,
438    BigDecimal,
439    Char,
440    NChar,
441    VarChar,
442    NVarChar,
443    BpChar,
444    Text,
445    MediumText,
446    LongText,
447    Blob,
448    MediumBlob,
449    LongBlob,
450    TinyBlob,
451    TinyText,
452    Name,
453    Binary,
454    VarBinary,
455    Json,
456    JsonB,
457    Time,
458    TimeTz,
459    TimeNs,
460    Timestamp,
461    TimestampTz,
462    TimestampLtz,
463    TimestampNtz,
464    TimestampS,
465    TimestampMs,
466    TimestampNs,
467    DateTime,
468    DateTime2,
469    DateTime64,
470    SmallDateTime,
471    Date,
472    Date32,
473    Int4Range,
474    Int4MultiRange,
475    Int8Range,
476    Int8MultiRange,
477    NumRange,
478    NumMultiRange,
479    TsRange,
480    TsMultiRange,
481    TsTzRange,
482    TsTzMultiRange,
483    DateRange,
484    DateMultiRange,
485    Uuid,
486    Geography,
487    GeographyPoint,
488    Nullable,
489    Geometry,
490    Point,
491    Ring,
492    LineString,
493    LocalTime,
494    LocalTimestamp,
495    SysTimestamp,
496    MultiLineString,
497    Polygon,
498    MultiPolygon,
499    HllSketch,
500    HStore,
501    Super,
502    Serial,
503    SmallSerial,
504    BigSerial,
505    Xml,
506    Year,
507    UserDefined,
508    Money,
509    SmallMoney,
510    RowVersion,
511    Image,
512    Variant,
513    Object,
514    Inet,
515    IpAddress,
516    IpPrefix,
517    Ipv4,
518    Ipv6,
519    Enum,
520    Enum8,
521    Enum16,
522    FixedString,
523    LowCardinality,
524    Nested,
525    AggregateFunction,
526    SimpleAggregateFunction,
527    TDigest,
528    Unknown,
529    Vector,
530    Dynamic,
531    Void,
532
533    // Keywords
534    Add,
535    Alias,
536    Alter,
537    All,
538    Anti,
539    Any,
540    Apply,
541    Array,
542    Asc,
543    AsOf,
544    Attach,
545    AutoIncrement,
546    Begin,
547    Between,
548    BulkCollectInto,
549    Cache,
550    Cascade,
551    Case,
552    CharacterSet,
553    Cluster,
554    ClusterBy,
555    Collate,
556    Command,
557    Comment,
558    Commit,
559    Prepare,
560    Preserve,
561    Connect,
562    ConnectBy,
563    Constraint,
564    Copy,
565    Create,
566    Cross,
567    Cube,
568    CurrentDate,
569    CurrentDateTime,
570    CurrentSchema,
571    CurrentTime,
572    CurrentTimestamp,
573    CurrentUser,
574    CurrentRole,
575    CurrentCatalog,
576    Declare,
577    Default,
578    Delete,
579    Desc,
580    Describe,
581    Detach,
582    Dictionary,
583    Distinct,
584    Distribute,
585    DistributeBy,
586    Div,
587    Drop,
588    Else,
589    End,
590    Escape,
591    Except,
592    Execute,
593    Exists,
594    False,
595    Fetch,
596    File,
597    FileFormat,
598    Filter,
599    Final,
600    First,
601    For,
602    Force,
603    ForeignKey,
604    Format,
605    From,
606    Full,
607    Function,
608    Get,
609    Glob,
610    Global,
611    Grant,
612    GroupBy,
613    GroupingSets,
614    Having,
615    Hint,
616    Ignore,
617    ILike,
618    In,
619    Index,
620    IndexedBy,
621    Inner,
622    Input,
623    Insert,
624    Install,
625    Intersect,
626    Interval,
627    Into,
628    Inpath,
629    InputFormat,
630    Introducer,
631    IRLike,
632    Is,
633    IsNull,
634    Join,
635    JoinMarker,
636    Keep,
637    Key,
638    Kill,
639    Lambda,
640    Language,
641    Lateral,
642    Left,
643    Like,
644    NotLike,   // !~~ operator (PostgreSQL)
645    NotILike,  // !~~* operator (PostgreSQL)
646    NotRLike,  // !~ operator (PostgreSQL)
647    NotIRLike, // !~* operator (PostgreSQL)
648    Limit,
649    List,
650    Load,
651    Local,
652    Lock,
653    Map,
654    Match,
655    MatchCondition,
656    MatchRecognize,
657    MemberOf,
658    Materialized,
659    Merge,
660    Mod,
661    Model,
662    Natural,
663    Next,
664    NoAction,
665    Nothing,
666    NotNull,
667    Null,
668    ObjectIdentifier,
669    Offset,
670    On,
671    Only,
672    Operator,
673    OrderBy,
674    OrderSiblingsBy,
675    Ordered,
676    Ordinality,
677    Out,
678    Outer,
679    Output,
680    Over,
681    Overlaps,
682    Overwrite,
683    Partition,
684    PartitionBy,
685    Percent,
686    Pivot,
687    Placeholder,
688    Positional,
689    Pragma,
690    Prewhere,
691    PrimaryKey,
692    Procedure,
693    Properties,
694    PseudoType,
695    Put,
696    Qualify,
697    Quote,
698    QDColon,
699    Range,
700    Recursive,
701    Refresh,
702    Rename,
703    Replace,
704    Returning,
705    Revoke,
706    References,
707    Restrict,
708    Right,
709    RLike,
710    Rollback,
711    Rollup,
712    Row,
713    Rows,
714    Select,
715    Semi,
716    Savepoint,
717    Separator,
718    Sequence,
719    Serde,
720    SerdeProperties,
721    Set,
722    Settings,
723    Show,
724    Siblings,
725    SimilarTo,
726    Some,
727    Sort,
728    SortBy,
729    SoundsLike,
730    StartWith,
731    StorageIntegration,
732    StraightJoin,
733    Struct,
734    Summarize,
735    TableSample,
736    Sample,
737    Bernoulli,
738    System,
739    Block,
740    Seed,
741    Repeatable,
742    Tag,
743    Temporary,
744    Transaction,
745    To,
746    Top,
747    Then,
748    True,
749    Truncate,
750    Uncache,
751    Union,
752    Unnest,
753    Unpivot,
754    Update,
755    Use,
756    Using,
757    Values,
758    View,
759    SemanticView,
760    Volatile,
761    When,
762    Where,
763    Window,
764    With,
765    Ties,
766    Exclude,
767    No,
768    Others,
769    Unique,
770    UtcDate,
771    UtcTime,
772    UtcTimestamp,
773    VersionSnapshot,
774    TimestampSnapshot,
775    Option,
776    Sink,
777    Source,
778    Analyze,
779    Namespace,
780    Export,
781    As,
782    By,
783    Nulls,
784    Respect,
785    Last,
786    If,
787    Cast,
788    TryCast,
789    SafeCast,
790    Count,
791    Extract,
792    Substring,
793    Trim,
794    Leading,
795    Trailing,
796    Both,
797    Position,
798    Overlaying,
799    Placing,
800    Treat,
801    Within,
802    Group,
803    Order,
804
805    // Window function keywords
806    Unbounded,
807    Preceding,
808    Following,
809    Current,
810    Groups,
811
812    // DDL-specific keywords (Phase 4)
813    Trigger,
814    Type,
815    Domain,
816    Returns,
817    Body,
818    Increment,
819    Minvalue,
820    Maxvalue,
821    Start,
822    Cycle,
823    NoCycle,
824    Prior,
825    Generated,
826    Identity,
827    Always,
828    // MATCH_RECOGNIZE tokens
829    Measures,
830    Pattern,
831    Define,
832    Running,
833    Owned,
834    After,
835    Before,
836    Instead,
837    Each,
838    Statement,
839    Referencing,
840    Old,
841    New,
842    Of,
843    Check,
844    Authorization,
845    Restart,
846
847    // Special
848    /// Never produced by [`Tokenizer`]: the token stream is not terminated, and the
849    /// parser detects the end of input from the stream length instead. A caller can
850    /// still put one at the end of a stream it builds itself and hands to
851    /// `Parser::new`, which the parser honours; see `explicit_eof_token_tests`.
852    Eof,
853}
854
855impl TokenType {
856    /// Check if this token type is a keyword that can be used as an identifier in certain contexts
857    pub fn is_keyword(&self) -> bool {
858        matches!(
859            self,
860            TokenType::Select
861                | TokenType::From
862                | TokenType::Where
863                | TokenType::And
864                | TokenType::Or
865                | TokenType::Not
866                | TokenType::In
867                | TokenType::Is
868                | TokenType::Null
869                | TokenType::True
870                | TokenType::False
871                | TokenType::As
872                | TokenType::On
873                | TokenType::Join
874                | TokenType::Left
875                | TokenType::Right
876                | TokenType::Inner
877                | TokenType::Outer
878                | TokenType::Full
879                | TokenType::Cross
880                | TokenType::Semi
881                | TokenType::Anti
882                | TokenType::Union
883                | TokenType::Except
884                | TokenType::Intersect
885                | TokenType::GroupBy
886                | TokenType::OrderBy
887                | TokenType::Having
888                | TokenType::Limit
889                | TokenType::Offset
890                | TokenType::Case
891                | TokenType::When
892                | TokenType::Then
893                | TokenType::Else
894                | TokenType::End
895                | TokenType::Create
896                | TokenType::Drop
897                | TokenType::Alter
898                | TokenType::Insert
899                | TokenType::Update
900                | TokenType::Delete
901                | TokenType::Into
902                | TokenType::Values
903                | TokenType::Set
904                | TokenType::With
905                | TokenType::Distinct
906                | TokenType::All
907                | TokenType::Exists
908                | TokenType::Between
909                | TokenType::Like
910                | TokenType::ILike
911                // Additional keywords that can be used as identifiers
912                | TokenType::Filter
913                | TokenType::Date
914                | TokenType::Timestamp
915                | TokenType::TimestampTz
916                | TokenType::Interval
917                | TokenType::Time
918                | TokenType::Table
919                | TokenType::Index
920                | TokenType::Column
921                | TokenType::Database
922                | TokenType::Schema
923                | TokenType::View
924                | TokenType::Function
925                | TokenType::Procedure
926                | TokenType::Trigger
927                | TokenType::Sequence
928                | TokenType::Over
929                | TokenType::Partition
930                | TokenType::Window
931                | TokenType::Rows
932                | TokenType::Range
933                | TokenType::First
934                | TokenType::Last
935                | TokenType::Preceding
936                | TokenType::Following
937                | TokenType::Current
938                | TokenType::Row
939                | TokenType::Unbounded
940                | TokenType::Array
941                | TokenType::Struct
942                | TokenType::Map
943                | TokenType::PrimaryKey
944                | TokenType::Key
945                | TokenType::ForeignKey
946                | TokenType::References
947                | TokenType::Unique
948                | TokenType::Check
949                | TokenType::Default
950                | TokenType::Constraint
951                | TokenType::Comment
952                | TokenType::Rollup
953                | TokenType::Cube
954                | TokenType::Grant
955                | TokenType::Revoke
956                | TokenType::Type
957                | TokenType::Use
958                | TokenType::Cache
959                | TokenType::Uncache
960                | TokenType::Load
961                | TokenType::Any
962                | TokenType::Some
963                | TokenType::Asc
964                | TokenType::Desc
965                | TokenType::Nulls
966                | TokenType::Lateral
967                | TokenType::Natural
968                | TokenType::Escape
969                | TokenType::Glob
970                | TokenType::Match
971                | TokenType::Recursive
972                | TokenType::Replace
973                | TokenType::Returns
974                | TokenType::If
975                | TokenType::Pivot
976                | TokenType::Unpivot
977                | TokenType::Json
978                | TokenType::Blob
979                | TokenType::Text
980                | TokenType::Int
981                | TokenType::BigInt
982                | TokenType::SmallInt
983                | TokenType::TinyInt
984                | TokenType::Int128
985                | TokenType::UInt128
986                | TokenType::Int256
987                | TokenType::UInt256
988                | TokenType::UInt
989                | TokenType::UBigInt
990                | TokenType::Float
991                | TokenType::Double
992                | TokenType::Decimal
993                | TokenType::Boolean
994                | TokenType::VarChar
995                | TokenType::Char
996                | TokenType::Binary
997                | TokenType::VarBinary
998                | TokenType::No
999                | TokenType::DateTime
1000                | TokenType::Truncate
1001                | TokenType::Execute
1002                | TokenType::Merge
1003                | TokenType::Top
1004                | TokenType::Begin
1005                | TokenType::Generated
1006                | TokenType::Identity
1007                | TokenType::Always
1008                | TokenType::Extract
1009                // Keywords that can be identifiers in certain contexts
1010                | TokenType::AsOf
1011                | TokenType::Prior
1012                | TokenType::After
1013                | TokenType::Restrict
1014                | TokenType::Cascade
1015                | TokenType::Local
1016                | TokenType::Rename
1017                | TokenType::Enum
1018                | TokenType::Within
1019                | TokenType::Format
1020                | TokenType::Final
1021                | TokenType::FileFormat
1022                | TokenType::Input
1023                | TokenType::InputFormat
1024                | TokenType::Copy
1025                | TokenType::Put
1026                | TokenType::Get
1027                | TokenType::Show
1028                | TokenType::Serde
1029                | TokenType::Sample
1030                | TokenType::Sort
1031                | TokenType::Collate
1032                | TokenType::Ties
1033                | TokenType::IsNull
1034                | TokenType::NotNull
1035                | TokenType::Exclude
1036                | TokenType::Temporary
1037                | TokenType::Add
1038                | TokenType::Ordinality
1039                | TokenType::Overlaps
1040                | TokenType::Block
1041                | TokenType::Pattern
1042                | TokenType::Group
1043                | TokenType::Cluster
1044                | TokenType::Repeatable
1045                | TokenType::Groups
1046                | TokenType::Commit
1047                | TokenType::Warehouse
1048                | TokenType::System
1049                | TokenType::By
1050                | TokenType::To
1051                | TokenType::Fetch
1052                | TokenType::For
1053                | TokenType::Only
1054                | TokenType::Next
1055                | TokenType::Lock
1056                | TokenType::Refresh
1057                | TokenType::Settings
1058                | TokenType::Operator
1059                | TokenType::Overwrite
1060                | TokenType::StraightJoin
1061                | TokenType::Start
1062                // Additional keywords registered in tokenizer but previously missing from is_keyword()
1063                | TokenType::Ignore
1064                | TokenType::Domain
1065                | TokenType::Apply
1066                | TokenType::Respect
1067                | TokenType::Materialized
1068                | TokenType::Prewhere
1069                | TokenType::Old
1070                | TokenType::New
1071                | TokenType::Cast
1072                | TokenType::TryCast
1073                | TokenType::SafeCast
1074                | TokenType::Transaction
1075                | TokenType::Describe
1076                | TokenType::Kill
1077                | TokenType::Lambda
1078                | TokenType::Declare
1079                | TokenType::Keep
1080                | TokenType::Output
1081                | TokenType::Percent
1082                | TokenType::Qualify
1083                | TokenType::Returning
1084                | TokenType::Language
1085                | TokenType::Prepare
1086                | TokenType::Preserve
1087                | TokenType::Savepoint
1088                | TokenType::Rollback
1089                | TokenType::Body
1090                | TokenType::Increment
1091                | TokenType::Minvalue
1092                | TokenType::Maxvalue
1093                | TokenType::Cycle
1094                | TokenType::NoCycle
1095                | TokenType::Seed
1096                | TokenType::Namespace
1097                | TokenType::Authorization
1098                | TokenType::Order
1099                | TokenType::Restart
1100                | TokenType::Before
1101                | TokenType::Instead
1102                | TokenType::Each
1103                | TokenType::Statement
1104                | TokenType::Referencing
1105                | TokenType::Of
1106                | TokenType::Separator
1107                | TokenType::Others
1108                | TokenType::Placing
1109                | TokenType::Owned
1110                | TokenType::Running
1111                | TokenType::Define
1112                | TokenType::Measures
1113                | TokenType::MatchRecognize
1114                | TokenType::AutoIncrement
1115                | TokenType::Connect
1116                | TokenType::Distribute
1117                | TokenType::Bernoulli
1118                | TokenType::TableSample
1119                | TokenType::Inpath
1120                | TokenType::Pragma
1121                | TokenType::Siblings
1122                | TokenType::SerdeProperties
1123                | TokenType::RLike
1124        )
1125    }
1126
1127    /// Check if this token type is a comparison operator
1128    pub fn is_comparison(&self) -> bool {
1129        matches!(
1130            self,
1131            TokenType::Eq
1132                | TokenType::Neq
1133                | TokenType::Lt
1134                | TokenType::Lte
1135                | TokenType::Gt
1136                | TokenType::Gte
1137                | TokenType::NullsafeEq
1138        )
1139    }
1140
1141    /// Check if this token type is an arithmetic operator
1142    pub fn is_arithmetic(&self) -> bool {
1143        matches!(
1144            self,
1145            TokenType::Plus
1146                | TokenType::Dash
1147                | TokenType::Star
1148                | TokenType::Slash
1149                | TokenType::Percent
1150                | TokenType::Mod
1151                | TokenType::Div
1152        )
1153    }
1154}
1155
1156impl fmt::Display for TokenType {
1157    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1158        write!(f, "{:?}", self)
1159    }
1160}
1161
1162// ── Cached default maps for TokenizerConfig ─────────────────────────────────
1163
1164static DEFAULT_KEYWORDS: LazyLock<HashMap<String, TokenType>> = LazyLock::new(|| {
1165    let mut keywords = HashMap::with_capacity(300);
1166    // Add basic SQL keywords
1167    keywords.insert("SELECT".to_string(), TokenType::Select);
1168    keywords.insert("FROM".to_string(), TokenType::From);
1169    keywords.insert("WHERE".to_string(), TokenType::Where);
1170    keywords.insert("AND".to_string(), TokenType::And);
1171    keywords.insert("OR".to_string(), TokenType::Or);
1172    keywords.insert("NOT".to_string(), TokenType::Not);
1173    keywords.insert("AS".to_string(), TokenType::As);
1174    keywords.insert("ON".to_string(), TokenType::On);
1175    keywords.insert("JOIN".to_string(), TokenType::Join);
1176    keywords.insert("LEFT".to_string(), TokenType::Left);
1177    keywords.insert("RIGHT".to_string(), TokenType::Right);
1178    keywords.insert("INNER".to_string(), TokenType::Inner);
1179    keywords.insert("OUTER".to_string(), TokenType::Outer);
1180    keywords.insert("OUTPUT".to_string(), TokenType::Output);
1181    keywords.insert("FULL".to_string(), TokenType::Full);
1182    keywords.insert("CROSS".to_string(), TokenType::Cross);
1183    keywords.insert("SEMI".to_string(), TokenType::Semi);
1184    keywords.insert("ANTI".to_string(), TokenType::Anti);
1185    keywords.insert("STRAIGHT_JOIN".to_string(), TokenType::StraightJoin);
1186    keywords.insert("UNION".to_string(), TokenType::Union);
1187    keywords.insert("EXCEPT".to_string(), TokenType::Except);
1188    keywords.insert("MINUS".to_string(), TokenType::Except); // Oracle/Redshift alias for EXCEPT
1189    keywords.insert("INTERSECT".to_string(), TokenType::Intersect);
1190    keywords.insert("GROUP".to_string(), TokenType::Group);
1191    keywords.insert("CUBE".to_string(), TokenType::Cube);
1192    keywords.insert("ROLLUP".to_string(), TokenType::Rollup);
1193    keywords.insert("WITHIN".to_string(), TokenType::Within);
1194    keywords.insert("ORDER".to_string(), TokenType::Order);
1195    keywords.insert("BY".to_string(), TokenType::By);
1196    keywords.insert("HAVING".to_string(), TokenType::Having);
1197    keywords.insert("LIMIT".to_string(), TokenType::Limit);
1198    keywords.insert("OFFSET".to_string(), TokenType::Offset);
1199    keywords.insert("ORDINALITY".to_string(), TokenType::Ordinality);
1200    keywords.insert("FETCH".to_string(), TokenType::Fetch);
1201    keywords.insert("FIRST".to_string(), TokenType::First);
1202    keywords.insert("NEXT".to_string(), TokenType::Next);
1203    keywords.insert("ONLY".to_string(), TokenType::Only);
1204    keywords.insert("KEEP".to_string(), TokenType::Keep);
1205    keywords.insert("IGNORE".to_string(), TokenType::Ignore);
1206    keywords.insert("INPUT".to_string(), TokenType::Input);
1207    keywords.insert("CASE".to_string(), TokenType::Case);
1208    keywords.insert("WHEN".to_string(), TokenType::When);
1209    keywords.insert("THEN".to_string(), TokenType::Then);
1210    keywords.insert("ELSE".to_string(), TokenType::Else);
1211    keywords.insert("END".to_string(), TokenType::End);
1212    keywords.insert("ENDIF".to_string(), TokenType::End); // Exasol alias for END
1213    keywords.insert("NULL".to_string(), TokenType::Null);
1214    keywords.insert("TRUE".to_string(), TokenType::True);
1215    keywords.insert("FALSE".to_string(), TokenType::False);
1216    keywords.insert("IS".to_string(), TokenType::Is);
1217    keywords.insert("IN".to_string(), TokenType::In);
1218    keywords.insert("BETWEEN".to_string(), TokenType::Between);
1219    keywords.insert("OVERLAPS".to_string(), TokenType::Overlaps);
1220    keywords.insert("LIKE".to_string(), TokenType::Like);
1221    keywords.insert("ILIKE".to_string(), TokenType::ILike);
1222    keywords.insert("RLIKE".to_string(), TokenType::RLike);
1223    keywords.insert("REGEXP".to_string(), TokenType::RLike);
1224    keywords.insert("ESCAPE".to_string(), TokenType::Escape);
1225    keywords.insert("EXISTS".to_string(), TokenType::Exists);
1226    keywords.insert("DISTINCT".to_string(), TokenType::Distinct);
1227    keywords.insert("ALL".to_string(), TokenType::All);
1228    keywords.insert("WITH".to_string(), TokenType::With);
1229    keywords.insert("CREATE".to_string(), TokenType::Create);
1230    keywords.insert("DROP".to_string(), TokenType::Drop);
1231    keywords.insert("ALTER".to_string(), TokenType::Alter);
1232    keywords.insert("TRUNCATE".to_string(), TokenType::Truncate);
1233    keywords.insert("TABLE".to_string(), TokenType::Table);
1234    keywords.insert("VIEW".to_string(), TokenType::View);
1235    keywords.insert("INDEX".to_string(), TokenType::Index);
1236    keywords.insert("COLUMN".to_string(), TokenType::Column);
1237    keywords.insert("CONSTRAINT".to_string(), TokenType::Constraint);
1238    keywords.insert("ADD".to_string(), TokenType::Add);
1239    keywords.insert("CASCADE".to_string(), TokenType::Cascade);
1240    keywords.insert("RESTRICT".to_string(), TokenType::Restrict);
1241    keywords.insert("RENAME".to_string(), TokenType::Rename);
1242    keywords.insert("TEMPORARY".to_string(), TokenType::Temporary);
1243    keywords.insert("TEMP".to_string(), TokenType::Temporary);
1244    keywords.insert("UNIQUE".to_string(), TokenType::Unique);
1245    keywords.insert("PRIMARY".to_string(), TokenType::PrimaryKey);
1246    keywords.insert("FOREIGN".to_string(), TokenType::ForeignKey);
1247    keywords.insert("KEY".to_string(), TokenType::Key);
1248    keywords.insert("KILL".to_string(), TokenType::Kill);
1249    keywords.insert("REFERENCES".to_string(), TokenType::References);
1250    keywords.insert("DEFAULT".to_string(), TokenType::Default);
1251    keywords.insert("DECLARE".to_string(), TokenType::Declare);
1252    keywords.insert("AUTO_INCREMENT".to_string(), TokenType::AutoIncrement);
1253    keywords.insert("AUTOINCREMENT".to_string(), TokenType::AutoIncrement); // Snowflake style
1254    keywords.insert("MATERIALIZED".to_string(), TokenType::Materialized);
1255    keywords.insert("REPLACE".to_string(), TokenType::Replace);
1256    keywords.insert("TO".to_string(), TokenType::To);
1257    keywords.insert("INSERT".to_string(), TokenType::Insert);
1258    keywords.insert("OVERWRITE".to_string(), TokenType::Overwrite);
1259    keywords.insert("UPDATE".to_string(), TokenType::Update);
1260    keywords.insert("USE".to_string(), TokenType::Use);
1261    keywords.insert("WAREHOUSE".to_string(), TokenType::Warehouse);
1262    keywords.insert("GLOB".to_string(), TokenType::Glob);
1263    keywords.insert("DELETE".to_string(), TokenType::Delete);
1264    keywords.insert("MERGE".to_string(), TokenType::Merge);
1265    keywords.insert("CACHE".to_string(), TokenType::Cache);
1266    keywords.insert("UNCACHE".to_string(), TokenType::Uncache);
1267    keywords.insert("REFRESH".to_string(), TokenType::Refresh);
1268    keywords.insert("GRANT".to_string(), TokenType::Grant);
1269    keywords.insert("REVOKE".to_string(), TokenType::Revoke);
1270    keywords.insert("COMMENT".to_string(), TokenType::Comment);
1271    keywords.insert("COLLATE".to_string(), TokenType::Collate);
1272    keywords.insert("INTO".to_string(), TokenType::Into);
1273    keywords.insert("VALUES".to_string(), TokenType::Values);
1274    keywords.insert("SET".to_string(), TokenType::Set);
1275    keywords.insert("SETTINGS".to_string(), TokenType::Settings);
1276    keywords.insert("SEPARATOR".to_string(), TokenType::Separator);
1277    keywords.insert("ASC".to_string(), TokenType::Asc);
1278    keywords.insert("DESC".to_string(), TokenType::Desc);
1279    keywords.insert("NULLS".to_string(), TokenType::Nulls);
1280    keywords.insert("RESPECT".to_string(), TokenType::Respect);
1281    keywords.insert("FIRST".to_string(), TokenType::First);
1282    keywords.insert("LAST".to_string(), TokenType::Last);
1283    keywords.insert("IF".to_string(), TokenType::If);
1284    keywords.insert("CAST".to_string(), TokenType::Cast);
1285    keywords.insert("TRY_CAST".to_string(), TokenType::TryCast);
1286    keywords.insert("SAFE_CAST".to_string(), TokenType::SafeCast);
1287    keywords.insert("OVER".to_string(), TokenType::Over);
1288    keywords.insert("PARTITION".to_string(), TokenType::Partition);
1289    keywords.insert("PLACING".to_string(), TokenType::Placing);
1290    keywords.insert("WINDOW".to_string(), TokenType::Window);
1291    keywords.insert("ROWS".to_string(), TokenType::Rows);
1292    keywords.insert("RANGE".to_string(), TokenType::Range);
1293    keywords.insert("FILTER".to_string(), TokenType::Filter);
1294    keywords.insert("NATURAL".to_string(), TokenType::Natural);
1295    keywords.insert("USING".to_string(), TokenType::Using);
1296    keywords.insert("UNBOUNDED".to_string(), TokenType::Unbounded);
1297    keywords.insert("PRECEDING".to_string(), TokenType::Preceding);
1298    keywords.insert("FOLLOWING".to_string(), TokenType::Following);
1299    keywords.insert("CURRENT".to_string(), TokenType::Current);
1300    keywords.insert("ROW".to_string(), TokenType::Row);
1301    keywords.insert("GROUPS".to_string(), TokenType::Groups);
1302    keywords.insert("RECURSIVE".to_string(), TokenType::Recursive);
1303    // TRIM function position keywords
1304    keywords.insert("BOTH".to_string(), TokenType::Both);
1305    keywords.insert("LEADING".to_string(), TokenType::Leading);
1306    keywords.insert("TRAILING".to_string(), TokenType::Trailing);
1307    keywords.insert("INTERVAL".to_string(), TokenType::Interval);
1308    // Phase 3: Additional keywords
1309    keywords.insert("TOP".to_string(), TokenType::Top);
1310    keywords.insert("QUALIFY".to_string(), TokenType::Qualify);
1311    keywords.insert("SAMPLE".to_string(), TokenType::Sample);
1312    keywords.insert("TABLESAMPLE".to_string(), TokenType::TableSample);
1313    keywords.insert("BERNOULLI".to_string(), TokenType::Bernoulli);
1314    keywords.insert("SYSTEM".to_string(), TokenType::System);
1315    keywords.insert("BLOCK".to_string(), TokenType::Block);
1316    keywords.insert("TIES".to_string(), TokenType::Ties);
1317    keywords.insert("LATERAL".to_string(), TokenType::Lateral);
1318    keywords.insert("LAMBDA".to_string(), TokenType::Lambda);
1319    keywords.insert("APPLY".to_string(), TokenType::Apply);
1320    // Oracle CONNECT BY keywords
1321    keywords.insert("CONNECT".to_string(), TokenType::Connect);
1322    // Hive/Spark specific keywords
1323    keywords.insert("CLUSTER".to_string(), TokenType::Cluster);
1324    keywords.insert("DISTRIBUTE".to_string(), TokenType::Distribute);
1325    keywords.insert("SORT".to_string(), TokenType::Sort);
1326    keywords.insert("PIVOT".to_string(), TokenType::Pivot);
1327    keywords.insert("PREWHERE".to_string(), TokenType::Prewhere);
1328    keywords.insert("UNPIVOT".to_string(), TokenType::Unpivot);
1329    keywords.insert("FOR".to_string(), TokenType::For);
1330    keywords.insert("ANY".to_string(), TokenType::Any);
1331    keywords.insert("SOME".to_string(), TokenType::Some);
1332    keywords.insert("ASOF".to_string(), TokenType::AsOf);
1333    keywords.insert("PERCENT".to_string(), TokenType::Percent);
1334    keywords.insert("EXCLUDE".to_string(), TokenType::Exclude);
1335    keywords.insert("NO".to_string(), TokenType::No);
1336    keywords.insert("OTHERS".to_string(), TokenType::Others);
1337    // PostgreSQL OPERATOR() syntax for schema-qualified operators
1338    keywords.insert("OPERATOR".to_string(), TokenType::Operator);
1339    // Phase 4: DDL keywords
1340    keywords.insert("SCHEMA".to_string(), TokenType::Schema);
1341    keywords.insert("NAMESPACE".to_string(), TokenType::Namespace);
1342    keywords.insert("DATABASE".to_string(), TokenType::Database);
1343    keywords.insert("FUNCTION".to_string(), TokenType::Function);
1344    keywords.insert("PROCEDURE".to_string(), TokenType::Procedure);
1345    keywords.insert("PROC".to_string(), TokenType::Procedure);
1346    keywords.insert("SEQUENCE".to_string(), TokenType::Sequence);
1347    keywords.insert("TRIGGER".to_string(), TokenType::Trigger);
1348    keywords.insert("TYPE".to_string(), TokenType::Type);
1349    keywords.insert("DOMAIN".to_string(), TokenType::Domain);
1350    keywords.insert("RETURNS".to_string(), TokenType::Returns);
1351    keywords.insert("RETURNING".to_string(), TokenType::Returning);
1352    keywords.insert("LANGUAGE".to_string(), TokenType::Language);
1353    keywords.insert("ROLLBACK".to_string(), TokenType::Rollback);
1354    keywords.insert("COMMIT".to_string(), TokenType::Commit);
1355    keywords.insert("BEGIN".to_string(), TokenType::Begin);
1356    keywords.insert("DESCRIBE".to_string(), TokenType::Describe);
1357    keywords.insert("PREPARE".to_string(), TokenType::Prepare);
1358    keywords.insert("PRESERVE".to_string(), TokenType::Preserve);
1359    keywords.insert("TRANSACTION".to_string(), TokenType::Transaction);
1360    keywords.insert("SAVEPOINT".to_string(), TokenType::Savepoint);
1361    keywords.insert("BODY".to_string(), TokenType::Body);
1362    keywords.insert("INCREMENT".to_string(), TokenType::Increment);
1363    keywords.insert("MINVALUE".to_string(), TokenType::Minvalue);
1364    keywords.insert("MAXVALUE".to_string(), TokenType::Maxvalue);
1365    keywords.insert("CYCLE".to_string(), TokenType::Cycle);
1366    keywords.insert("NOCYCLE".to_string(), TokenType::NoCycle);
1367    keywords.insert("PRIOR".to_string(), TokenType::Prior);
1368    // MATCH_RECOGNIZE keywords
1369    keywords.insert("MATCH".to_string(), TokenType::Match);
1370    keywords.insert("MATCH_RECOGNIZE".to_string(), TokenType::MatchRecognize);
1371    keywords.insert("MEASURES".to_string(), TokenType::Measures);
1372    keywords.insert("PATTERN".to_string(), TokenType::Pattern);
1373    keywords.insert("DEFINE".to_string(), TokenType::Define);
1374    keywords.insert("RUNNING".to_string(), TokenType::Running);
1375    keywords.insert("FINAL".to_string(), TokenType::Final);
1376    keywords.insert("OWNED".to_string(), TokenType::Owned);
1377    keywords.insert("AFTER".to_string(), TokenType::After);
1378    keywords.insert("BEFORE".to_string(), TokenType::Before);
1379    keywords.insert("INSTEAD".to_string(), TokenType::Instead);
1380    keywords.insert("EACH".to_string(), TokenType::Each);
1381    keywords.insert("STATEMENT".to_string(), TokenType::Statement);
1382    keywords.insert("REFERENCING".to_string(), TokenType::Referencing);
1383    keywords.insert("OLD".to_string(), TokenType::Old);
1384    keywords.insert("NEW".to_string(), TokenType::New);
1385    keywords.insert("OF".to_string(), TokenType::Of);
1386    keywords.insert("CHECK".to_string(), TokenType::Check);
1387    keywords.insert("START".to_string(), TokenType::Start);
1388    keywords.insert("ENUM".to_string(), TokenType::Enum);
1389    keywords.insert("AUTHORIZATION".to_string(), TokenType::Authorization);
1390    keywords.insert("RESTART".to_string(), TokenType::Restart);
1391    // Date/time literal keywords
1392    keywords.insert("DATE".to_string(), TokenType::Date);
1393    keywords.insert("TIME".to_string(), TokenType::Time);
1394    keywords.insert("TIMESTAMP".to_string(), TokenType::Timestamp);
1395    keywords.insert("DATETIME".to_string(), TokenType::DateTime);
1396    keywords.insert("GENERATED".to_string(), TokenType::Generated);
1397    keywords.insert("IDENTITY".to_string(), TokenType::Identity);
1398    keywords.insert("ALWAYS".to_string(), TokenType::Always);
1399    // LOAD DATA keywords
1400    keywords.insert("LOAD".to_string(), TokenType::Load);
1401    keywords.insert("LOCAL".to_string(), TokenType::Local);
1402    keywords.insert("INPATH".to_string(), TokenType::Inpath);
1403    keywords.insert("INPUTFORMAT".to_string(), TokenType::InputFormat);
1404    keywords.insert("SERDE".to_string(), TokenType::Serde);
1405    keywords.insert("SERDEPROPERTIES".to_string(), TokenType::SerdeProperties);
1406    keywords.insert("FORMAT".to_string(), TokenType::Format);
1407    // SQLite
1408    keywords.insert("PRAGMA".to_string(), TokenType::Pragma);
1409    // SHOW statement
1410    keywords.insert("SHOW".to_string(), TokenType::Show);
1411    // Oracle ORDER SIBLINGS BY (hierarchical queries)
1412    keywords.insert("SIBLINGS".to_string(), TokenType::Siblings);
1413    // COPY and PUT statements (Snowflake, PostgreSQL)
1414    keywords.insert("COPY".to_string(), TokenType::Copy);
1415    keywords.insert("PUT".to_string(), TokenType::Put);
1416    keywords.insert("GET".to_string(), TokenType::Get);
1417    // EXEC/EXECUTE statement (TSQL, etc.)
1418    keywords.insert("EXEC".to_string(), TokenType::Execute);
1419    keywords.insert("EXECUTE".to_string(), TokenType::Execute);
1420    // Postfix null check operators (PostgreSQL/SQLite)
1421    keywords.insert("ISNULL".to_string(), TokenType::IsNull);
1422    keywords.insert("NOTNULL".to_string(), TokenType::NotNull);
1423    keywords
1424});
1425
1426static DEFAULT_SINGLE_TOKENS: LazyLock<HashMap<char, TokenType>> = LazyLock::new(|| {
1427    let mut single_tokens = HashMap::with_capacity(30);
1428    single_tokens.insert('(', TokenType::LParen);
1429    single_tokens.insert(')', TokenType::RParen);
1430    single_tokens.insert('[', TokenType::LBracket);
1431    single_tokens.insert(']', TokenType::RBracket);
1432    single_tokens.insert('{', TokenType::LBrace);
1433    single_tokens.insert('}', TokenType::RBrace);
1434    single_tokens.insert(',', TokenType::Comma);
1435    single_tokens.insert('.', TokenType::Dot);
1436    single_tokens.insert(';', TokenType::Semicolon);
1437    single_tokens.insert('+', TokenType::Plus);
1438    single_tokens.insert('-', TokenType::Dash);
1439    single_tokens.insert('*', TokenType::Star);
1440    single_tokens.insert('/', TokenType::Slash);
1441    single_tokens.insert('%', TokenType::Percent);
1442    single_tokens.insert('&', TokenType::Amp);
1443    single_tokens.insert('|', TokenType::Pipe);
1444    single_tokens.insert('^', TokenType::Caret);
1445    single_tokens.insert('~', TokenType::Tilde);
1446    single_tokens.insert('<', TokenType::Lt);
1447    single_tokens.insert('>', TokenType::Gt);
1448    single_tokens.insert('=', TokenType::Eq);
1449    single_tokens.insert('!', TokenType::Exclamation);
1450    single_tokens.insert(':', TokenType::Colon);
1451    single_tokens.insert('@', TokenType::DAt);
1452    single_tokens.insert('#', TokenType::Hash);
1453    single_tokens.insert('$', TokenType::Dollar);
1454    single_tokens.insert('?', TokenType::Parameter);
1455    single_tokens
1456});
1457
1458static DEFAULT_QUOTES: LazyLock<HashMap<String, String>> = LazyLock::new(|| {
1459    let mut quotes = HashMap::with_capacity(4);
1460    quotes.insert("'".to_string(), "'".to_string());
1461    // Triple-quoted strings (e.g., """x""")
1462    quotes.insert("\"\"\"".to_string(), "\"\"\"".to_string());
1463    quotes
1464});
1465
1466static DEFAULT_IDENTIFIERS: LazyLock<HashMap<char, char>> = LazyLock::new(|| {
1467    let mut identifiers = HashMap::with_capacity(4);
1468    identifiers.insert('"', '"');
1469    identifiers.insert('`', '`');
1470    // Note: TSQL bracket-quoted identifiers [name] are handled in the parser
1471    // because [ is also used for arrays and subscripts
1472    identifiers
1473});
1474
1475static DEFAULT_COMMENTS: LazyLock<HashMap<String, Option<String>>> = LazyLock::new(|| {
1476    let mut comments = HashMap::with_capacity(4);
1477    comments.insert("--".to_string(), None);
1478    comments.insert("/*".to_string(), Some("*/".to_string()));
1479    comments
1480});
1481
1482/// Tokenizer configuration for a dialect
1483#[derive(Debug, Clone)]
1484pub struct TokenizerConfig {
1485    /// Keywords mapping (uppercase keyword -> token type)
1486    pub keywords: HashMap<String, TokenType>,
1487    /// Single character tokens
1488    pub single_tokens: HashMap<char, TokenType>,
1489    /// Quote characters (start -> end)
1490    pub quotes: HashMap<String, String>,
1491    /// Identifier quote characters (start -> end)
1492    pub identifiers: HashMap<char, char>,
1493    /// Whether quoted identifiers use string-style backslash escapes (BigQuery/ClickHouse).
1494    pub identifier_backslash_escapes: bool,
1495    /// Comment definitions (start -> optional end)
1496    pub comments: HashMap<String, Option<String>>,
1497    /// String escape characters
1498    pub string_escapes: Vec<char>,
1499    /// Whether to support nested comments
1500    pub nested_comments: bool,
1501    /// Valid escape follow characters (for MySQL-style escaping).
1502    /// When a backslash is followed by a character NOT in this list,
1503    /// the backslash is discarded. When empty, all backslash escapes
1504    /// preserve the backslash for unrecognized sequences.
1505    pub escape_follow_chars: Vec<char>,
1506    /// Whether b'...' is a byte string (true for BigQuery) or bit string (false for standard SQL).
1507    /// Default is false (bit string).
1508    pub b_prefix_is_byte_string: bool,
1509    /// Numeric literal suffixes (uppercase suffix -> type name), e.g. {"L": "BIGINT", "S": "SMALLINT"}
1510    /// Used by Hive/Spark to parse 1L as CAST(1 AS BIGINT)
1511    pub numeric_literals: HashMap<String, String>,
1512    /// Whether unquoted identifiers can start with a digit (e.g., `1a`, `1_a`).
1513    /// When true, a number followed by letters/underscore is treated as an identifier.
1514    /// Used by Hive, Spark, MySQL, ClickHouse.
1515    pub identifiers_can_start_with_digit: bool,
1516    /// Whether 0x/0X prefix should be treated as hex literals.
1517    /// When true, `0XCC` is tokenized instead of Number("0") + Identifier("XCC").
1518    /// Used by BigQuery, SQLite, Teradata.
1519    pub hex_number_strings: bool,
1520    /// Whether hex string literals from 0x prefix represent integer values.
1521    /// When true (BigQuery), 0xA is tokenized as HexNumber (integer in hex notation).
1522    /// When false (SQLite, Teradata), 0xCC is tokenized as HexString (binary/blob value).
1523    pub hex_string_is_integer_type: bool,
1524    /// Whether an empty 0x/0X prefix is a valid binary string literal.
1525    /// Used by T-SQL, where `0x` represents an empty binary string.
1526    pub allow_empty_hex_string: bool,
1527    /// Whether string escape sequences (like \') are allowed in raw strings.
1528    /// When true (BigQuery default), \' inside r'...' escapes the quote.
1529    /// When false (Spark/Databricks), backslashes in raw strings are always literal.
1530    /// Python sqlglot: STRING_ESCAPES_ALLOWED_IN_RAW_STRINGS (default True)
1531    pub string_escapes_allowed_in_raw_strings: bool,
1532    /// Whether # starts a single-line comment (ClickHouse, MySQL)
1533    pub hash_comments: bool,
1534    /// Whether $ can start/continue an identifier (ClickHouse).
1535    /// When true, a bare `$` that is not part of a dollar-quoted string or positional
1536    /// parameter is treated as an identifier character.
1537    pub dollar_sign_is_identifier: bool,
1538    /// Whether INSERT ... FORMAT <name> should treat subsequent data as raw (ClickHouse).
1539    /// When true, after tokenizing `INSERT ... FORMAT <non-VALUES-name>`, all text until
1540    /// the next blank line or end of input is consumed as a raw data token.
1541    pub insert_format_raw_data: bool,
1542    /// Whether numeric literals can contain underscores as digit separators.
1543    /// When true, `1_000` is tokenized as `1000`. Used by ClickHouse and DuckDB.
1544    /// Python sqlglot: NUMBERS_CAN_BE_UNDERSCORE_SEPARATED (default False)
1545    pub numbers_can_be_underscore_separated: bool,
1546    /// Recover strings like `'a\' or 1=1` by treating the escaped quote as the
1547    /// closing quote when no later quote exists. This matches SQLGlot's permissive
1548    /// handling for a few malformed ClickHouse SHOW LIKE fixtures.
1549    pub recover_terminal_backslash_quote: bool,
1550    /// Recover a terminal single-quoted string without a closing quote by treating
1551    /// end-of-input as the close. This is only enabled for ClickHouse fixture
1552    /// coverage, where some extracted corpus rows contain partial string probes.
1553    pub recover_unterminated_string: bool,
1554}
1555
1556impl Default for TokenizerConfig {
1557    fn default() -> Self {
1558        Self {
1559            keywords: DEFAULT_KEYWORDS.clone(),
1560            single_tokens: DEFAULT_SINGLE_TOKENS.clone(),
1561            quotes: DEFAULT_QUOTES.clone(),
1562            identifiers: DEFAULT_IDENTIFIERS.clone(),
1563            identifier_backslash_escapes: false,
1564            comments: DEFAULT_COMMENTS.clone(),
1565            // Standard SQL: only '' (doubled quote) escapes a quote
1566            // Backslash escapes are dialect-specific (MySQL, etc.)
1567            string_escapes: vec!['\''],
1568            nested_comments: true,
1569            // By default, no escape_follow_chars means preserve backslash for unrecognized escapes
1570            escape_follow_chars: vec![],
1571            // Default: b'...' is bit string (standard SQL), not byte string (BigQuery)
1572            b_prefix_is_byte_string: false,
1573            numeric_literals: HashMap::new(),
1574            identifiers_can_start_with_digit: false,
1575            hex_number_strings: false,
1576            hex_string_is_integer_type: false,
1577            allow_empty_hex_string: false,
1578            // Default: backslash escapes ARE allowed in raw strings (sqlglot default)
1579            // Spark/Databricks set this to false
1580            string_escapes_allowed_in_raw_strings: true,
1581            hash_comments: false,
1582            dollar_sign_is_identifier: false,
1583            insert_format_raw_data: false,
1584            numbers_can_be_underscore_separated: false,
1585            recover_terminal_backslash_quote: false,
1586            recover_unterminated_string: false,
1587        }
1588    }
1589}
1590
1591/// SQL Tokenizer
1592pub struct Tokenizer {
1593    config: Arc<TokenizerConfig>,
1594}
1595
1596impl Tokenizer {
1597    /// Create a new tokenizer with the given configuration
1598    pub fn new(config: TokenizerConfig) -> Self {
1599        Self {
1600            config: Arc::new(config),
1601        }
1602    }
1603
1604    pub(crate) fn from_shared_config(config: Arc<TokenizerConfig>) -> Self {
1605        Self { config }
1606    }
1607
1608    /// Create a tokenizer with default configuration
1609    pub fn default_config() -> Self {
1610        Self::new(TokenizerConfig::default())
1611    }
1612
1613    /// Tokenize a SQL string
1614    pub fn tokenize(&self, sql: &str) -> Result<Vec<Token>> {
1615        if sql.is_ascii() {
1616            TokenizerState::<_, Token>::new(sql, &self.config, AsciiCursor(sql.as_bytes()))
1617                .tokenize()
1618        } else {
1619            TokenizerState::<_, Token>::new(sql, &self.config, UnicodeCursor::new(sql)).tokenize()
1620        }
1621    }
1622
1623    pub(crate) fn tokenize_for_parser(
1624        &self,
1625        sql: &Arc<str>,
1626    ) -> Result<(Vec<ParserToken>, TokenGuardStats)> {
1627        if sql.is_ascii() {
1628            let mut state = TokenizerState::<_, ParserToken>::new_shared(
1629                sql,
1630                Arc::clone(sql),
1631                &self.config,
1632                AsciiCursor(sql.as_bytes()),
1633            );
1634            let tokens = state.tokenize()?;
1635            Ok((tokens, state.guard_stats.take().unwrap_or_default()))
1636        } else {
1637            let mut state = TokenizerState::<_, ParserToken>::new_shared(
1638                sql,
1639                Arc::clone(sql),
1640                &self.config,
1641                UnicodeCursor::new(sql),
1642            );
1643            let tokens = state.tokenize()?;
1644            Ok((tokens, state.guard_stats.take().unwrap_or_default()))
1645        }
1646    }
1647
1648    #[cfg(test)]
1649    fn tokenize_without_ascii_fast_path(&self, sql: &str) -> Result<Vec<Token>> {
1650        TokenizerState::new(sql, &self.config, UnicodeCursor::new(sql)).tokenize()
1651    }
1652
1653    #[cfg(test)]
1654    pub(crate) fn shares_config_with(&self, other: &Self) -> bool {
1655        Arc::ptr_eq(&self.config, &other.config)
1656    }
1657}
1658
1659impl Default for Tokenizer {
1660    fn default() -> Self {
1661        Self::default_config()
1662    }
1663}
1664
1665trait TokenizerCursor {
1666    fn len(&self) -> usize;
1667    fn char_at(&self, index: usize) -> char;
1668    fn text_from_range(&self, source: &str, start: usize, end: usize) -> String;
1669
1670    fn source_range<'a>(&self, _source: &'a str, _start: usize, _end: usize) -> Option<&'a str> {
1671        None
1672    }
1673
1674    fn range_contains(&self, start: usize, needle: char) -> bool {
1675        (start..self.len()).any(|index| self.char_at(index) == needle)
1676    }
1677}
1678
1679struct AsciiCursor<'a>(&'a [u8]);
1680
1681impl TokenizerCursor for AsciiCursor<'_> {
1682    #[inline]
1683    fn len(&self) -> usize {
1684        self.0.len()
1685    }
1686
1687    #[inline]
1688    fn char_at(&self, index: usize) -> char {
1689        self.0[index] as char
1690    }
1691
1692    #[inline]
1693    fn text_from_range(&self, source: &str, start: usize, end: usize) -> String {
1694        source[start..end].to_string()
1695    }
1696
1697    #[inline]
1698    fn source_range<'a>(&self, source: &'a str, start: usize, end: usize) -> Option<&'a str> {
1699        Some(&source[start..end])
1700    }
1701}
1702
1703struct UnicodeCursor(Vec<char>);
1704
1705impl UnicodeCursor {
1706    fn new(source: &str) -> Self {
1707        Self(source.chars().collect())
1708    }
1709}
1710
1711impl TokenizerCursor for UnicodeCursor {
1712    #[inline]
1713    fn len(&self) -> usize {
1714        self.0.len()
1715    }
1716
1717    #[inline]
1718    fn char_at(&self, index: usize) -> char {
1719        self.0[index]
1720    }
1721
1722    #[inline]
1723    fn text_from_range(&self, _source: &str, start: usize, end: usize) -> String {
1724        self.0[start..end].iter().collect()
1725    }
1726}
1727
1728/// Internal state for tokenization
1729struct TokenizerState<'a, C, T> {
1730    source: &'a str,
1731    shared_source: Option<Arc<str>>,
1732    cursor: C,
1733    size: usize,
1734    tokens: Vec<T>,
1735    start: usize,
1736    current: usize,
1737    line: usize,
1738    column: usize,
1739    comments: Vec<String>,
1740    guard_stats: Option<TokenGuardStats>,
1741    config: &'a TokenizerConfig,
1742}
1743
1744impl<'a, C: TokenizerCursor, T: TokenOutput> TokenizerState<'a, C, T> {
1745    fn new(sql: &'a str, config: &'a TokenizerConfig, cursor: C) -> Self {
1746        let size = cursor.len();
1747        Self {
1748            source: sql,
1749            shared_source: None,
1750            cursor,
1751            size,
1752            tokens: Vec::new(),
1753            start: 0,
1754            current: 0,
1755            line: 1,
1756            column: 1,
1757            comments: Vec::new(),
1758            guard_stats: None,
1759            config,
1760        }
1761    }
1762
1763    fn new_shared(sql: &'a str, source: Arc<str>, config: &'a TokenizerConfig, cursor: C) -> Self {
1764        let size = cursor.len();
1765        Self {
1766            source: sql,
1767            shared_source: Some(source),
1768            cursor,
1769            size,
1770            tokens: Vec::new(),
1771            start: 0,
1772            current: 0,
1773            line: 1,
1774            column: 1,
1775            comments: Vec::new(),
1776            guard_stats: Some(TokenGuardStats::default()),
1777            config,
1778        }
1779    }
1780
1781    fn tokenize(&mut self) -> Result<Vec<T>> {
1782        while !self.is_at_end() {
1783            self.skip_whitespace();
1784            if self.is_at_end() {
1785                break;
1786            }
1787
1788            self.start = self.current;
1789            self.scan_token()?;
1790
1791            // ClickHouse: After INSERT ... FORMAT <name> (where name != VALUES),
1792            // the rest until the next blank line or end of input is raw data.
1793            if self.config.insert_format_raw_data {
1794                if let Some(raw) = self.try_scan_insert_format_raw_data() {
1795                    if !raw.is_empty() {
1796                        self.start = self.current;
1797                        self.add_token_with_text(TokenType::Var, raw);
1798                    }
1799                }
1800            }
1801        }
1802
1803        // Handle leftover leading comments at end of input.
1804        // These are comments on a new line after the last token that couldn't be attached
1805        // as leading comments to a subsequent token (because there is none).
1806        // Attach them as trailing comments on the last token so they're preserved.
1807        if !self.comments.is_empty() {
1808            if let Some(last) = self.tokens.last_mut() {
1809                last.trailing_comments_mut().extend(self.comments.drain(..));
1810            }
1811        }
1812
1813        Ok(std::mem::take(&mut self.tokens))
1814    }
1815
1816    #[inline]
1817    fn is_at_end(&self) -> bool {
1818        self.current >= self.size
1819    }
1820
1821    #[inline]
1822    fn text_from_range(&self, start: usize, end: usize) -> String {
1823        self.cursor.text_from_range(self.source, start, end)
1824    }
1825
1826    #[inline]
1827    fn char_at(&self, index: usize) -> char {
1828        self.cursor.char_at(index)
1829    }
1830
1831    #[inline]
1832    fn range_contains(&self, start: usize, needle: char) -> bool {
1833        self.cursor.range_contains(start, needle)
1834    }
1835
1836    #[inline]
1837    fn peek(&self) -> char {
1838        if self.is_at_end() {
1839            '\0'
1840        } else {
1841            self.char_at(self.current)
1842        }
1843    }
1844
1845    #[inline]
1846    fn peek_next(&self) -> char {
1847        if self.current + 1 >= self.size {
1848            '\0'
1849        } else {
1850            self.char_at(self.current + 1)
1851        }
1852    }
1853
1854    #[inline]
1855    fn advance(&mut self) -> char {
1856        let c = self.peek();
1857        self.current += 1;
1858        if c == '\n' {
1859            self.line += 1;
1860            self.column = 1;
1861        } else {
1862            self.column += 1;
1863        }
1864        c
1865    }
1866
1867    #[inline]
1868    fn advance_ascii_to(&mut self, end: usize) -> bool {
1869        let Some(text) = self.cursor.source_range(self.source, self.current, end) else {
1870            return false;
1871        };
1872
1873        let newline_count = text
1874            .as_bytes()
1875            .iter()
1876            .filter(|&&byte| byte == b'\n')
1877            .count();
1878        if newline_count == 0 {
1879            self.column += end - self.current;
1880        } else {
1881            self.line += newline_count;
1882            let last_newline = text
1883                .as_bytes()
1884                .iter()
1885                .rposition(|&byte| byte == b'\n')
1886                .expect("newline count is non-zero");
1887            self.column = text.len() - last_newline;
1888        }
1889        self.current = end;
1890        true
1891    }
1892
1893    #[inline]
1894    fn advance_ascii_digits(&mut self) -> bool {
1895        let Some(rest) = self
1896            .cursor
1897            .source_range(self.source, self.current, self.size)
1898        else {
1899            return false;
1900        };
1901        let bytes = rest.as_bytes();
1902        let mut length = 0;
1903        while length < bytes.len() {
1904            match bytes[length] {
1905                b'0'..=b'9' => length += 1,
1906                b'_' if bytes.get(length + 1).is_some_and(u8::is_ascii_digit) => length += 1,
1907                _ => break,
1908            }
1909        }
1910        self.current += length;
1911        self.column += length;
1912        true
1913    }
1914
1915    #[inline]
1916    fn advance_ascii_hex_digits(&mut self) -> bool {
1917        let Some(rest) = self
1918            .cursor
1919            .source_range(self.source, self.current, self.size)
1920        else {
1921            return false;
1922        };
1923        let bytes = rest.as_bytes();
1924        let mut length = 0;
1925        while length < bytes.len() {
1926            match bytes[length] {
1927                byte if byte.is_ascii_hexdigit() => length += 1,
1928                b'_' if bytes.get(length + 1).is_some_and(u8::is_ascii_hexdigit) => length += 1,
1929                _ => break,
1930            }
1931        }
1932        self.current += length;
1933        self.column += length;
1934        true
1935    }
1936
1937    #[inline]
1938    fn advance_ascii_identifier(&mut self) -> bool {
1939        let Some(rest) = self
1940            .cursor
1941            .source_range(self.source, self.current, self.size)
1942        else {
1943            return false;
1944        };
1945        let bytes = rest.as_bytes();
1946        let mut length = 0;
1947        while length < bytes.len() {
1948            let byte = bytes[length];
1949            if byte == b'#' && matches!(bytes.get(length + 1), Some(b'>') | Some(b'-')) {
1950                break;
1951            }
1952            if byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'$' | b'#' | b'@') {
1953                length += 1;
1954            } else {
1955                break;
1956            }
1957        }
1958        self.current += length;
1959        self.column += length;
1960        true
1961    }
1962
1963    fn try_scan_simple_quoted_content(
1964        &mut self,
1965        quote: char,
1966        backslash_is_escape: bool,
1967    ) -> Option<(usize, usize)> {
1968        let content_start = self.current;
1969        let rest = self
1970            .cursor
1971            .source_range(self.source, content_start, self.size)?;
1972        let quote_offset = rest.find(quote)?;
1973        let content_end = content_start + quote_offset;
1974
1975        if (content_end + 1 < self.size && self.char_at(content_end + 1) == quote)
1976            || (backslash_is_escape && rest[..quote_offset].contains('\\'))
1977        {
1978            return None;
1979        }
1980
1981        self.advance_ascii_to(content_end);
1982        self.advance();
1983        Some((content_start, content_end))
1984    }
1985
1986    fn skip_whitespace(&mut self) {
1987        // Track whether we've seen a newline since the last token.
1988        // Comments on a new line (after a newline) are leading comments on the next token,
1989        // while comments on the same line are trailing comments on the previous token.
1990        // This matches Python sqlglot's behavior.
1991        let mut saw_newline = false;
1992        while !self.is_at_end() {
1993            let c = self.peek();
1994            match c {
1995                ' ' | '\t' | '\r' => {
1996                    self.advance();
1997                }
1998                '\n' => {
1999                    saw_newline = true;
2000                    self.advance();
2001                }
2002                '\u{00A0}' // non-breaking space
2003                | '\u{2000}'..='\u{200B}' // various Unicode spaces + zero-width space
2004                | '\u{3000}' // ideographic (full-width) space
2005                | '\u{FEFF}' // BOM / zero-width no-break space
2006                => {
2007                    self.advance();
2008                }
2009                '-' if self.peek_next() == '-' => {
2010                    self.scan_line_comment(saw_newline);
2011                    // After a line comment, we're always on a new line
2012                    saw_newline = true;
2013                }
2014                '/' if self.peek_next() == '/' && self.config.hash_comments => {
2015                    // ClickHouse: // single-line comments (same dialects that support # comments)
2016                    self.scan_double_slash_comment();
2017                }
2018                '/' if self.peek_next() == '*' => {
2019                    // Check if this is a hint comment /*+ ... */
2020                    if self.current + 2 < self.size && self.char_at(self.current + 2) == '+' {
2021                        // This is a hint comment, handle it as a token instead of skipping
2022                        break;
2023                    }
2024                    if self.scan_block_comment(saw_newline).is_err() {
2025                        return;
2026                    }
2027                    // Don't reset saw_newline - it carries forward
2028                }
2029                '/' if self.peek_next() == '/' && self.config.comments.contains_key("//") => {
2030                    // Dialect-specific // line comment (e.g., Snowflake)
2031                    // But NOT inside URIs like file:// or paths with consecutive slashes
2032                    // Check that previous non-whitespace char is not ':' or '/'
2033                    let prev_non_ws = if self.current > 0 {
2034                        let mut i = self.current - 1;
2035                        while i > 0 && (self.char_at(i) == ' ' || self.char_at(i) == '\t') {
2036                            i -= 1;
2037                        }
2038                        self.char_at(i)
2039                    } else {
2040                        '\0'
2041                    };
2042                    if prev_non_ws == ':' || prev_non_ws == '/' {
2043                        // This is likely a URI (file://, http://) or path, not a comment
2044                        break;
2045                    }
2046                    self.scan_line_comment(saw_newline);
2047                    // After a line comment, we're always on a new line
2048                    saw_newline = true;
2049                }
2050                '#' if self.config.hash_comments => {
2051                    self.scan_hash_line_comment();
2052                }
2053                _ => break,
2054            }
2055        }
2056    }
2057
2058    fn scan_hash_line_comment(&mut self) {
2059        self.advance(); // #
2060        let start = self.current;
2061        while !self.is_at_end() && self.peek() != '\n' {
2062            self.advance();
2063        }
2064        let comment = self.text_from_range(start, self.current);
2065        let comment_text = comment.trim().to_string();
2066        if let Some(last) = self.tokens.last_mut() {
2067            last.trailing_comments_mut().push(comment_text);
2068        } else {
2069            self.comments.push(comment_text);
2070        }
2071    }
2072
2073    fn scan_double_slash_comment(&mut self) {
2074        self.advance(); // /
2075        self.advance(); // /
2076        let start = self.current;
2077        while !self.is_at_end() && self.peek() != '\n' {
2078            self.advance();
2079        }
2080        let comment = self.text_from_range(start, self.current);
2081        let comment_text = comment.trim().to_string();
2082        if let Some(last) = self.tokens.last_mut() {
2083            last.trailing_comments_mut().push(comment_text);
2084        } else {
2085            self.comments.push(comment_text);
2086        }
2087    }
2088
2089    fn scan_line_comment(&mut self, after_newline: bool) {
2090        self.advance(); // -
2091        self.advance(); // -
2092        let start = self.current;
2093        while !self.is_at_end() && self.peek() != '\n' {
2094            self.advance();
2095        }
2096        let comment_text = self.text_from_range(start, self.current);
2097
2098        // If the comment starts on a new line (after_newline), it's a leading comment
2099        // on the next token. Otherwise, it's a trailing comment on the previous token.
2100        if after_newline || self.tokens.is_empty() {
2101            self.comments.push(comment_text);
2102        } else if let Some(last) = self.tokens.last_mut() {
2103            last.trailing_comments_mut().push(comment_text);
2104        }
2105    }
2106
2107    fn scan_block_comment(&mut self, after_newline: bool) -> Result<()> {
2108        self.advance(); // /
2109        self.advance(); // *
2110        let content_start = self.current;
2111        let mut depth = 1;
2112
2113        while !self.is_at_end() && depth > 0 {
2114            if self.peek() == '/' && self.peek_next() == '*' && self.config.nested_comments {
2115                self.advance();
2116                self.advance();
2117                depth += 1;
2118            } else if self.peek() == '*' && self.peek_next() == '/' {
2119                depth -= 1;
2120                if depth > 0 {
2121                    self.advance();
2122                    self.advance();
2123                }
2124            } else {
2125                self.advance();
2126            }
2127        }
2128
2129        if depth > 0 {
2130            return Err(Error::tokenize(
2131                "Unterminated block comment",
2132                self.line,
2133                self.column,
2134                self.start,
2135                self.current,
2136            ));
2137        }
2138
2139        // Get the content between /* and */ (preserving internal whitespace for nested comments)
2140        let content = self.text_from_range(content_start, self.current);
2141        self.advance(); // *
2142        self.advance(); // /
2143
2144        // For round-trip fidelity, preserve the exact comment content including nested comments
2145        let comment_text = format!("/*{}*/", content);
2146
2147        // If the comment starts on a new line (after_newline), it's a leading comment
2148        // on the next token. Otherwise, it's a trailing comment on the previous token.
2149        if after_newline || self.tokens.is_empty() {
2150            self.comments.push(comment_text);
2151        } else if let Some(last) = self.tokens.last_mut() {
2152            last.trailing_comments_mut().push(comment_text);
2153        }
2154
2155        Ok(())
2156    }
2157
2158    /// Scan a hint comment /*+ ... */ and return it as a Hint token
2159    fn scan_hint(&mut self) -> Result<()> {
2160        self.advance(); // /
2161        self.advance(); // *
2162        self.advance(); // +
2163        let hint_start = self.current;
2164
2165        // Scan until we find */
2166        while !self.is_at_end() {
2167            if self.peek() == '*' && self.peek_next() == '/' {
2168                break;
2169            }
2170            self.advance();
2171        }
2172
2173        if self.is_at_end() {
2174            return Err(Error::tokenize(
2175                "Unterminated hint comment",
2176                self.line,
2177                self.column,
2178                self.start,
2179                self.current,
2180            ));
2181        }
2182
2183        let hint_text = self.text_from_range(hint_start, self.current);
2184        self.advance(); // *
2185        self.advance(); // /
2186
2187        self.add_token_with_text(TokenType::Hint, hint_text.trim().to_string());
2188
2189        Ok(())
2190    }
2191
2192    /// Scan a positional parameter: $1, $2, etc.
2193    fn scan_positional_parameter(&mut self) -> Result<()> {
2194        self.advance(); // consume $
2195        let start = self.current;
2196
2197        while !self.is_at_end() && self.peek().is_ascii_digit() {
2198            self.advance();
2199        }
2200
2201        let number = self.text_from_range(start, self.current);
2202        self.add_token_with_text(TokenType::Parameter, number);
2203        Ok(())
2204    }
2205
2206    /// Try to scan a tagged dollar-quoted string: $tag$content$tag$
2207    /// Returns Some(()) if successful, None if this isn't a tagged dollar string.
2208    ///
2209    /// The token text is stored as "tag\x00content" to preserve the tag for later use.
2210    fn try_scan_tagged_dollar_string(&mut self) -> Result<Option<()>> {
2211        let saved_pos = self.current;
2212
2213        // We're at '$', next char is alphabetic
2214        self.advance(); // consume opening $
2215
2216        // Scan the tag (identifier: alphanumeric + underscore, including Unicode)
2217        // Tags can contain Unicode characters like emojis (e.g., $🦆$)
2218        let tag_start = self.current;
2219        while !self.is_at_end()
2220            && (self.peek().is_alphanumeric() || self.peek() == '_' || !self.peek().is_ascii())
2221        {
2222            self.advance();
2223        }
2224        let tag = self.text_from_range(tag_start, self.current);
2225
2226        // Must have a closing $ after the tag
2227        if self.is_at_end() || self.peek() != '$' {
2228            // Not a tagged dollar string - restore position
2229            self.current = saved_pos;
2230            return Ok(None);
2231        }
2232        self.advance(); // consume closing $ of opening tag
2233
2234        // Now scan content until we find $tag$
2235        let content_start = self.current;
2236        let closing_tag = format!("${}$", tag);
2237        let closing_chars: Vec<char> = closing_tag.chars().collect();
2238
2239        loop {
2240            if self.is_at_end() {
2241                // Unterminated - restore and fall through
2242                self.current = saved_pos;
2243                return Ok(None);
2244            }
2245
2246            // Check if we've reached the closing tag
2247            if self.peek() == '$' && self.current + closing_chars.len() <= self.size {
2248                let matches = closing_chars.iter().enumerate().all(|(j, &ch)| {
2249                    self.current + j < self.size && self.char_at(self.current + j) == ch
2250                });
2251                if matches {
2252                    let content = self.text_from_range(content_start, self.current);
2253                    // Consume closing tag
2254                    for _ in 0..closing_chars.len() {
2255                        self.advance();
2256                    }
2257                    // Store as "tag\x00content" to preserve the tag
2258                    let token_text = format!("{}\x00{}", tag, content);
2259                    self.add_token_with_text(TokenType::DollarString, token_text);
2260                    return Ok(Some(()));
2261                }
2262            }
2263            self.advance();
2264        }
2265    }
2266
2267    /// Scan a dollar-quoted string: $$content$$ or $tag$content$tag$
2268    ///
2269    /// For $$...$$ (no tag), the token text is just the content.
2270    /// For $tag$...$tag$, use try_scan_tagged_dollar_string instead.
2271    fn scan_dollar_quoted_string(&mut self) -> Result<()> {
2272        self.advance(); // consume first $
2273        self.advance(); // consume second $
2274
2275        // For $$...$$ (no tag), just scan until closing $$
2276        let start = self.current;
2277        while !self.is_at_end() {
2278            if self.peek() == '$'
2279                && self.current + 1 < self.size
2280                && self.char_at(self.current + 1) == '$'
2281            {
2282                break;
2283            }
2284            self.advance();
2285        }
2286
2287        let content = self.text_from_range(start, self.current);
2288
2289        if !self.is_at_end() {
2290            self.advance(); // consume first $
2291            self.advance(); // consume second $
2292        }
2293
2294        self.add_token_with_text(TokenType::DollarString, content);
2295        Ok(())
2296    }
2297
2298    fn scan_token(&mut self) -> Result<()> {
2299        let c = self.peek();
2300
2301        // Check for string literal
2302        if c == '\'' {
2303            // Check for triple-quoted string '''...''' if configured
2304            if self.config.quotes.contains_key("'''")
2305                && self.peek_next() == '\''
2306                && self.current + 2 < self.size
2307                && self.char_at(self.current + 2) == '\''
2308            {
2309                return self.scan_triple_quoted_string('\'');
2310            }
2311            return self.scan_string();
2312        }
2313
2314        // Check for triple-quoted string """...""" if configured
2315        if c == '"'
2316            && self.config.quotes.contains_key("\"\"\"")
2317            && self.peek_next() == '"'
2318            && self.current + 2 < self.size
2319            && self.char_at(self.current + 2) == '"'
2320        {
2321            return self.scan_triple_quoted_string('"');
2322        }
2323
2324        // Check for double-quoted strings when dialect supports them (e.g., BigQuery)
2325        // This must come before identifier quotes check
2326        if c == '"'
2327            && self.config.quotes.contains_key("\"")
2328            && !self.config.identifiers.contains_key(&'"')
2329        {
2330            return self.scan_double_quoted_string();
2331        }
2332
2333        // Check for identifier quotes
2334        if let Some(&end_quote) = self.config.identifiers.get(&c) {
2335            return self.scan_quoted_identifier(end_quote);
2336        }
2337
2338        // Check for numbers (including numbers starting with a dot like .25)
2339        if c.is_ascii_digit() {
2340            return self.scan_number();
2341        }
2342
2343        // Check for numbers starting with a dot (e.g., .25, .5)
2344        // This must come before single character token handling
2345        // Don't treat as a number if:
2346        // - Previous char was also a dot (e.g., 1..2 should be 1, ., ., 2)
2347        // - Previous char is an identifier character (e.g., foo.25 should be foo, ., 25)
2348        //   This handles BigQuery numeric table parts like project.dataset.25
2349        if c == '.' && self.peek_next().is_ascii_digit() {
2350            let prev_char = if self.current > 0 {
2351                self.char_at(self.current - 1)
2352            } else {
2353                '\0'
2354            };
2355            let is_after_ident = prev_char.is_alphanumeric()
2356                || prev_char == '_'
2357                || prev_char == '`'
2358                || prev_char == '"'
2359                || prev_char == ']'
2360                || prev_char == ')';
2361            if prev_char != '.' && !is_after_ident {
2362                return self.scan_number_starting_with_dot();
2363            }
2364        }
2365
2366        // Check for hint comment /*+ ... */
2367        if c == '/'
2368            && self.peek_next() == '*'
2369            && self.current + 2 < self.size
2370            && self.char_at(self.current + 2) == '+'
2371        {
2372            return self.scan_hint();
2373        }
2374
2375        // Check for multi-character operators first
2376        if let Some(token_type) = self.try_scan_multi_char_operator() {
2377            self.add_token(token_type);
2378            return Ok(());
2379        }
2380
2381        // Check for tagged dollar-quoted strings: $tag$content$tag$
2382        // Tags can contain Unicode characters (including emojis like 🦆) and digits (e.g., $1$)
2383        if c == '$'
2384            && (self.peek_next().is_alphanumeric()
2385                || self.peek_next() == '_'
2386                || !self.peek_next().is_ascii())
2387        {
2388            if let Some(()) = self.try_scan_tagged_dollar_string()? {
2389                return Ok(());
2390            }
2391            // If tagged dollar string didn't match and dollar_sign_is_identifier is set,
2392            // treat the $ and following chars as an identifier (e.g., ClickHouse $alias$name$).
2393            if self.config.dollar_sign_is_identifier {
2394                return self.scan_dollar_identifier();
2395            }
2396        }
2397
2398        // Check for dollar-quoted strings: $$...$$
2399        if c == '$' && self.peek_next() == '$' {
2400            return self.scan_dollar_quoted_string();
2401        }
2402
2403        // Check for positional parameters: $1, $2, etc.
2404        if c == '$' && self.peek_next().is_ascii_digit() {
2405            return self.scan_positional_parameter();
2406        }
2407
2408        // ClickHouse: bare $ (not followed by alphanumeric/underscore) as identifier
2409        if c == '$' && self.config.dollar_sign_is_identifier {
2410            return self.scan_dollar_identifier();
2411        }
2412
2413        // TSQL: Check for identifiers starting with # (temp tables) or @ (variables)
2414        // e.g., #temp, ##global_temp, @variable
2415        if (c == '#' || c == '@')
2416            && (self.peek_next().is_alphanumeric()
2417                || self.peek_next() == '_'
2418                || self.peek_next() == '#')
2419        {
2420            return self.scan_tsql_identifier();
2421        }
2422
2423        // Check for single character tokens
2424        if let Some(&token_type) = self.config.single_tokens.get(&c) {
2425            self.advance();
2426            self.add_token(token_type);
2427            return Ok(());
2428        }
2429
2430        // Unicode minus (U+2212) → treat as regular minus
2431        if c == '\u{2212}' {
2432            self.advance();
2433            self.add_token(TokenType::Dash);
2434            return Ok(());
2435        }
2436
2437        // Unicode fraction slash (U+2044) → treat as regular slash
2438        if c == '\u{2044}' {
2439            self.advance();
2440            self.add_token(TokenType::Slash);
2441            return Ok(());
2442        }
2443
2444        // Unicode curly/smart quotes → treat as regular string quotes
2445        if c == '\u{2018}' || c == '\u{2019}' {
2446            // Left/right single quotation marks → scan as string with matching end
2447            return self.scan_unicode_quoted_string(c);
2448        }
2449        if c == '\u{201C}' || c == '\u{201D}' {
2450            // Left/right double quotation marks → scan as quoted identifier
2451            return self.scan_unicode_quoted_identifier(c);
2452        }
2453
2454        // Must be an identifier or keyword
2455        self.scan_identifier_or_keyword()
2456    }
2457
2458    fn try_scan_multi_char_operator(&mut self) -> Option<TokenType> {
2459        let c = self.peek();
2460        let next = self.peek_next();
2461        let third = if self.current + 2 < self.size {
2462            self.char_at(self.current + 2)
2463        } else {
2464            '\0'
2465        };
2466
2467        // Check for three-character operators first
2468        // -|- (Adjacent - PostgreSQL range adjacency)
2469        if c == '-' && next == '|' && third == '-' {
2470            self.advance();
2471            self.advance();
2472            self.advance();
2473            return Some(TokenType::Adjacent);
2474        }
2475
2476        // ||/ (Cube root - PostgreSQL)
2477        if c == '|' && next == '|' && third == '/' {
2478            self.advance();
2479            self.advance();
2480            self.advance();
2481            return Some(TokenType::DPipeSlash);
2482        }
2483
2484        // #>> (JSONB path text extraction - PostgreSQL)
2485        if c == '#' && next == '>' && third == '>' {
2486            self.advance();
2487            self.advance();
2488            self.advance();
2489            return Some(TokenType::DHashArrow);
2490        }
2491
2492        // ->> (JSON text extraction - PostgreSQL/MySQL)
2493        if c == '-' && next == '>' && third == '>' {
2494            self.advance();
2495            self.advance();
2496            self.advance();
2497            return Some(TokenType::DArrow);
2498        }
2499
2500        // <=> (NULL-safe equality - MySQL)
2501        if c == '<' && next == '=' && third == '>' {
2502            self.advance();
2503            self.advance();
2504            self.advance();
2505            return Some(TokenType::NullsafeEq);
2506        }
2507
2508        // <-> (Distance operator - PostgreSQL)
2509        if c == '<' && next == '-' && third == '>' {
2510            self.advance();
2511            self.advance();
2512            self.advance();
2513            return Some(TokenType::LrArrow);
2514        }
2515
2516        // <@ (Contained by - PostgreSQL)
2517        if c == '<' && next == '@' {
2518            self.advance();
2519            self.advance();
2520            return Some(TokenType::LtAt);
2521        }
2522
2523        // @> (Contains - PostgreSQL)
2524        if c == '@' && next == '>' {
2525            self.advance();
2526            self.advance();
2527            return Some(TokenType::AtGt);
2528        }
2529
2530        // ~~~ (Glob - PostgreSQL)
2531        if c == '~' && next == '~' && third == '~' {
2532            self.advance();
2533            self.advance();
2534            self.advance();
2535            return Some(TokenType::Glob);
2536        }
2537
2538        // ~~* (ILike - PostgreSQL)
2539        if c == '~' && next == '~' && third == '*' {
2540            self.advance();
2541            self.advance();
2542            self.advance();
2543            return Some(TokenType::ILike);
2544        }
2545
2546        // !~~* (Not ILike - PostgreSQL)
2547        let fourth = if self.current + 3 < self.size {
2548            self.char_at(self.current + 3)
2549        } else {
2550            '\0'
2551        };
2552        if c == '!' && next == '~' && third == '~' && fourth == '*' {
2553            self.advance();
2554            self.advance();
2555            self.advance();
2556            self.advance();
2557            return Some(TokenType::NotILike);
2558        }
2559
2560        // !~~ (Not Like - PostgreSQL)
2561        if c == '!' && next == '~' && third == '~' {
2562            self.advance();
2563            self.advance();
2564            self.advance();
2565            return Some(TokenType::NotLike);
2566        }
2567
2568        // !~* (Not Regexp ILike - PostgreSQL)
2569        if c == '!' && next == '~' && third == '*' {
2570            self.advance();
2571            self.advance();
2572            self.advance();
2573            return Some(TokenType::NotIRLike);
2574        }
2575
2576        // !:> (Not cast / Try cast - SingleStore)
2577        if c == '!' && next == ':' && third == '>' {
2578            self.advance();
2579            self.advance();
2580            self.advance();
2581            return Some(TokenType::NColonGt);
2582        }
2583
2584        // ?:: (TRY_CAST shorthand - Databricks)
2585        if c == '?' && next == ':' && third == ':' {
2586            self.advance();
2587            self.advance();
2588            self.advance();
2589            return Some(TokenType::QDColon);
2590        }
2591
2592        // !~ (Not Regexp - PostgreSQL)
2593        if c == '!' && next == '~' {
2594            self.advance();
2595            self.advance();
2596            return Some(TokenType::NotRLike);
2597        }
2598
2599        // ~~ (Like - PostgreSQL)
2600        if c == '~' && next == '~' {
2601            self.advance();
2602            self.advance();
2603            return Some(TokenType::Like);
2604        }
2605
2606        // ~* (Regexp ILike - PostgreSQL)
2607        if c == '~' && next == '*' {
2608            self.advance();
2609            self.advance();
2610            return Some(TokenType::IRLike);
2611        }
2612
2613        // SingleStore three-character JSON path operators (must be checked before :: two-char)
2614        // ::$ (JSON extract string), ::% (JSON extract double), ::? (JSON match)
2615        if c == ':' && next == ':' && third == '$' {
2616            self.advance();
2617            self.advance();
2618            self.advance();
2619            return Some(TokenType::DColonDollar);
2620        }
2621        if c == ':' && next == ':' && third == '%' {
2622            self.advance();
2623            self.advance();
2624            self.advance();
2625            return Some(TokenType::DColonPercent);
2626        }
2627        if c == ':' && next == ':' && third == '?' {
2628            self.advance();
2629            self.advance();
2630            self.advance();
2631            return Some(TokenType::DColonQMark);
2632        }
2633
2634        // Two-character operators
2635        let token_type = match (c, next) {
2636            ('.', ':') => Some(TokenType::DotColon),
2637            ('=', '=') => Some(TokenType::Eq), // Hive/Spark == equality operator
2638            ('<', '=') => Some(TokenType::Lte),
2639            ('>', '=') => Some(TokenType::Gte),
2640            ('!', '=') => Some(TokenType::Neq),
2641            ('<', '>') => Some(TokenType::Neq),
2642            ('^', '=') => Some(TokenType::Neq),
2643            ('<', '<') => Some(TokenType::LtLt),
2644            ('>', '>') => Some(TokenType::GtGt),
2645            ('|', '|') => Some(TokenType::DPipe),
2646            ('|', '/') => Some(TokenType::PipeSlash), // Square root - PostgreSQL
2647            (':', ':') => Some(TokenType::DColon),
2648            (':', '=') => Some(TokenType::ColonEq), // := (assignment, named args)
2649            (':', '>') => Some(TokenType::ColonGt), // ::> (TSQL)
2650            ('-', '>') => Some(TokenType::Arrow),   // JSON object access
2651            ('=', '>') => Some(TokenType::FArrow),  // Fat arrow (lambda)
2652            ('&', '&') => Some(TokenType::DAmp),
2653            ('&', '<') => Some(TokenType::AmpLt), // PostgreSQL range operator
2654            ('&', '>') => Some(TokenType::AmpGt), // PostgreSQL range operator
2655            ('@', '@') => Some(TokenType::AtAt),  // Text search match
2656            ('@', '?') => Some(TokenType::AtQMark), // JSON path exists - PostgreSQL
2657            ('?', '|') => Some(TokenType::QMarkPipe), // JSONB contains any key
2658            ('?', '&') => Some(TokenType::QMarkAmp), // JSONB contains all keys
2659            ('?', '?') => Some(TokenType::DQMark), // Double question mark
2660            ('#', '>') => Some(TokenType::HashArrow), // JSONB path extraction
2661            ('#', '-') => Some(TokenType::HashDash), // JSONB delete
2662            ('^', '@') => Some(TokenType::CaretAt), // PostgreSQL starts-with operator
2663            ('*', '*') => Some(TokenType::DStar), // Power operator
2664            ('|', '>') => Some(TokenType::PipeGt), // Pipe-greater (some dialects)
2665            _ => None,
2666        };
2667
2668        if token_type.is_some() {
2669            self.advance();
2670            self.advance();
2671        }
2672
2673        token_type
2674    }
2675
2676    fn scan_string(&mut self) -> Result<()> {
2677        self.advance(); // Opening quote
2678        if let Some((text_start, text_end)) =
2679            self.try_scan_simple_quoted_content('\'', self.config.string_escapes.contains(&'\\'))
2680        {
2681            self.add_token_from_source(TokenType::String, text_start, text_end);
2682            return Ok(());
2683        }
2684        let mut value = String::new();
2685
2686        while !self.is_at_end() {
2687            let c = self.peek();
2688            if c == '\'' {
2689                if self.peek_next() == '\'' {
2690                    // Escaped quote
2691                    value.push('\'');
2692                    self.advance();
2693                    self.advance();
2694                } else {
2695                    break;
2696                }
2697            } else if c == '\\' && self.config.string_escapes.contains(&'\\') {
2698                if self.config.recover_terminal_backslash_quote
2699                    && self.peek_next() == '\''
2700                    && !self.range_contains(self.current + 2, '\'')
2701                {
2702                    value.push(self.advance());
2703                    break;
2704                }
2705
2706                self.scan_backslash_escape(&mut value);
2707            } else {
2708                value.push(self.advance());
2709            }
2710        }
2711
2712        if self.is_at_end() {
2713            if self.config.recover_unterminated_string {
2714                self.add_token_with_text(TokenType::String, value);
2715                return Ok(());
2716            }
2717
2718            return Err(Error::tokenize(
2719                "Unterminated string",
2720                self.line,
2721                self.column,
2722                self.start,
2723                self.current,
2724            ));
2725        }
2726
2727        self.advance(); // Closing quote
2728        self.add_token_with_text(TokenType::String, value);
2729        Ok(())
2730    }
2731
2732    /// Scan a double-quoted string (for dialects like BigQuery where " is a string delimiter)
2733    fn scan_double_quoted_string(&mut self) -> Result<()> {
2734        self.advance(); // Opening quote
2735        let mut value = String::new();
2736
2737        while !self.is_at_end() {
2738            let c = self.peek();
2739            if c == '"' {
2740                if self.peek_next() == '"' {
2741                    // Escaped quote
2742                    value.push('"');
2743                    self.advance();
2744                    self.advance();
2745                } else {
2746                    break;
2747                }
2748            } else if c == '\\' && self.config.string_escapes.contains(&'\\') {
2749                self.scan_backslash_escape(&mut value);
2750            } else {
2751                value.push(self.advance());
2752            }
2753        }
2754
2755        if self.is_at_end() {
2756            return Err(Error::tokenize(
2757                "Unterminated double-quoted string",
2758                self.line,
2759                self.column,
2760                self.start,
2761                self.current,
2762            ));
2763        }
2764
2765        self.advance(); // Closing quote
2766        self.add_token_with_text(TokenType::String, value);
2767        Ok(())
2768    }
2769
2770    fn scan_backslash_escape(&mut self, value: &mut String) {
2771        self.advance(); // Backslash
2772        if self.is_at_end() {
2773            value.push('\\');
2774            return;
2775        }
2776
2777        let escaped = self.advance();
2778        let restricted = !self.config.escape_follow_chars.is_empty();
2779        let always_allowed = matches!(escaped, '\\' | '\'' | '"');
2780        if restricted && !always_allowed && !self.config.escape_follow_chars.contains(&escaped) {
2781            value.push(escaped);
2782            return;
2783        }
2784
2785        let supports_octal =
2786            restricted && ('1'..='7').any(|digit| self.config.escape_follow_chars.contains(&digit));
2787        if supports_octal && escaped.is_digit(8) {
2788            if let Some(codepoint) = self.peek_radix_digits(2, 8).and_then(|suffix| {
2789                let first = escaped.to_digit(8)?;
2790                first.checked_mul(64)?.checked_add(suffix)
2791            }) {
2792                if let Ok(byte) = u8::try_from(codepoint) {
2793                    self.advance_count(2);
2794                    value.push(byte as char);
2795                    return;
2796                }
2797            }
2798
2799            if escaped == '0' {
2800                value.push('\0');
2801            } else {
2802                value.push(escaped);
2803            }
2804            return;
2805        }
2806
2807        match escaped {
2808            'n' => value.push('\n'),
2809            'r' => value.push('\r'),
2810            't' => value.push('\t'),
2811            '0' => value.push('\0'),
2812            'Z' => value.push('\x1A'),
2813            'a' => value.push('\x07'),
2814            'b' => value.push('\x08'),
2815            'f' => value.push('\x0C'),
2816            'v' => value.push('\x0B'),
2817            'x' => {
2818                if let Some(codepoint) = self.peek_radix_digits(2, 16) {
2819                    self.advance_count(2);
2820                    value.push(codepoint as u8 as char);
2821                } else if restricted {
2822                    // Invalid Snowflake numeric escapes are ordinary unknown escapes.
2823                    value.push('x');
2824                } else {
2825                    // Preserve the existing permissive behavior for dialects without
2826                    // an explicit escape-follow policy.
2827                    value.push('\\');
2828                    value.push('x');
2829                    for _ in 0..2 {
2830                        if !self.is_at_end() && self.peek().is_ascii_hexdigit() {
2831                            value.push(self.advance());
2832                        }
2833                    }
2834                }
2835            }
2836            'u' if restricted && self.config.escape_follow_chars.contains(&'u') => {
2837                if let Some(codepoint) = self.peek_radix_digits(4, 16).and_then(char::from_u32) {
2838                    self.advance_count(4);
2839                    value.push(codepoint);
2840                } else {
2841                    value.push('u');
2842                }
2843            }
2844            '\\' => value.push('\\'),
2845            '\'' => value.push('\''),
2846            '"' => value.push('"'),
2847            '%' => value.push('%'),
2848            '_' => value.push('_'),
2849            _ if restricted => value.push(escaped),
2850            _ => {
2851                value.push('\\');
2852                value.push(escaped);
2853            }
2854        }
2855    }
2856
2857    fn peek_radix_digits(&self, count: usize, radix: u32) -> Option<u32> {
2858        if self.current + count > self.size {
2859            return None;
2860        }
2861
2862        let mut value = 0_u32;
2863        for offset in 0..count {
2864            value = value
2865                .checked_mul(radix)?
2866                .checked_add(self.char_at(self.current + offset).to_digit(radix)?)?;
2867        }
2868        Some(value)
2869    }
2870
2871    fn advance_count(&mut self, count: usize) {
2872        for _ in 0..count {
2873            self.advance();
2874        }
2875    }
2876
2877    fn scan_triple_quoted_string(&mut self, quote_char: char) -> Result<()> {
2878        // Advance past the three opening quotes
2879        self.advance();
2880        self.advance();
2881        self.advance();
2882        let mut value = String::new();
2883
2884        while !self.is_at_end() {
2885            // Check for closing triple quote
2886            if self.peek() == quote_char
2887                && self.current + 1 < self.size
2888                && self.char_at(self.current + 1) == quote_char
2889                && self.current + 2 < self.size
2890                && self.char_at(self.current + 2) == quote_char
2891            {
2892                // Found closing """
2893                break;
2894            }
2895            if self.peek() == '\\' && self.config.string_escapes.contains(&'\\') {
2896                self.scan_backslash_escape(&mut value);
2897            } else {
2898                value.push(self.advance());
2899            }
2900        }
2901
2902        if self.is_at_end() {
2903            return Err(Error::tokenize(
2904                "Unterminated triple-quoted string",
2905                self.line,
2906                self.column,
2907                self.start,
2908                self.current,
2909            ));
2910        }
2911
2912        // Advance past the three closing quotes
2913        self.advance();
2914        self.advance();
2915        self.advance();
2916        let token_type = if quote_char == '"' {
2917            TokenType::TripleDoubleQuotedString
2918        } else {
2919            TokenType::TripleSingleQuotedString
2920        };
2921        self.add_token_with_text(token_type, value);
2922        Ok(())
2923    }
2924
2925    fn scan_quoted_identifier(&mut self, end_quote: char) -> Result<()> {
2926        self.advance(); // Opening quote
2927        let mut value = String::new();
2928
2929        loop {
2930            if self.is_at_end() {
2931                return Err(Error::tokenize(
2932                    "Unterminated identifier",
2933                    self.line,
2934                    self.column,
2935                    self.start,
2936                    self.current,
2937                ));
2938            }
2939            if self.config.identifier_backslash_escapes && self.peek() == '\\' {
2940                if self.peek_next() == end_quote {
2941                    value.push(end_quote);
2942                    self.advance_count(2);
2943                } else {
2944                    self.scan_backslash_escape(&mut value);
2945                }
2946                continue;
2947            }
2948            if self.peek() == end_quote {
2949                if self.peek_next() == end_quote {
2950                    // Escaped quote (e.g., "" inside "x""y") -> store single quote
2951                    value.push(end_quote);
2952                    self.advance(); // skip first quote
2953                    self.advance(); // skip second quote
2954                } else {
2955                    // End of identifier
2956                    break;
2957                }
2958            } else {
2959                value.push(self.peek());
2960                self.advance();
2961            }
2962        }
2963
2964        self.advance(); // Closing quote
2965        self.add_token_with_text(TokenType::QuotedIdentifier, value);
2966        Ok(())
2967    }
2968
2969    /// Scan a string delimited by Unicode curly single quotes (U+2018/U+2019).
2970    /// Content between curly quotes is literal (no escape processing).
2971    /// When opened with \u{2018} (left), close with \u{2019} (right) only.
2972    /// When opened with \u{2019} (right), close with \u{2019} (right) — self-closing.
2973    fn scan_unicode_quoted_string(&mut self, open_quote: char) -> Result<()> {
2974        self.advance(); // Opening curly quote
2975        let start = self.current;
2976        // Determine closing quote: left opens -> right closes; right opens -> right closes
2977        let close_quote = if open_quote == '\u{2018}' {
2978            '\u{2019}' // left opens, right closes
2979        } else {
2980            '\u{2019}' // right quote also closes with right quote
2981        };
2982        while !self.is_at_end() && self.peek() != close_quote {
2983            self.advance();
2984        }
2985        let value = self.text_from_range(start, self.current);
2986        if !self.is_at_end() {
2987            self.advance(); // Closing quote
2988        }
2989        self.add_token_with_text(TokenType::String, value);
2990        Ok(())
2991    }
2992
2993    /// Scan an identifier delimited by Unicode curly double quotes (U+201C/U+201D).
2994    /// When opened with \u{201C} (left), close with \u{201D} (right) only.
2995    fn scan_unicode_quoted_identifier(&mut self, open_quote: char) -> Result<()> {
2996        self.advance(); // Opening curly quote
2997        let start = self.current;
2998        let close_quote = if open_quote == '\u{201C}' {
2999            '\u{201D}' // left opens, right closes
3000        } else {
3001            '\u{201D}' // right also closes with right
3002        };
3003        while !self.is_at_end() && self.peek() != close_quote && self.peek() != '"' {
3004            self.advance();
3005        }
3006        let value = self.text_from_range(start, self.current);
3007        if !self.is_at_end() {
3008            self.advance(); // Closing quote
3009        }
3010        self.add_token_with_text(TokenType::QuotedIdentifier, value);
3011        Ok(())
3012    }
3013
3014    fn scan_number(&mut self) -> Result<()> {
3015        // Check for 0x/0X hex number prefix (SQLite-style)
3016        if self.config.hex_number_strings && self.peek() == '0' && !self.is_at_end() {
3017            let next = if self.current + 1 < self.size {
3018                self.char_at(self.current + 1)
3019            } else {
3020                '\0'
3021            };
3022            if next == 'x' || next == 'X' {
3023                // Advance past '0' and 'x'/'X'
3024                self.advance();
3025                self.advance();
3026                // Collect hex digits (allow underscores as separators, e.g., 0xbad_cafe)
3027                let hex_start = self.current;
3028                if !self.advance_ascii_hex_digits() {
3029                    while !self.is_at_end()
3030                        && (self.peek().is_ascii_hexdigit() || self.peek() == '_')
3031                    {
3032                        if self.peek() == '_' && !self.peek_next().is_ascii_hexdigit() {
3033                            break;
3034                        }
3035                        self.advance();
3036                    }
3037                }
3038                let has_hex_digits = self.current > hex_start;
3039                let next_is_identifier_part = {
3040                    let next = self.peek();
3041                    next.is_alphanumeric() || matches!(next, '_' | '$' | '#' | '@')
3042                };
3043                let is_empty_hex_string = self.config.allow_empty_hex_string
3044                    && !self.config.hex_string_is_integer_type
3045                    && !next_is_identifier_part;
3046                if has_hex_digits || is_empty_hex_string {
3047                    // Check for hex float: 0xABC.DEFpEXP or 0xABCpEXP
3048                    let mut is_hex_float = false;
3049                    // Optional fractional part: .hexdigits
3050                    if has_hex_digits && !self.is_at_end() && self.peek() == '.' {
3051                        let after_dot = if self.current + 1 < self.size {
3052                            self.char_at(self.current + 1)
3053                        } else {
3054                            '\0'
3055                        };
3056                        if after_dot.is_ascii_hexdigit() {
3057                            is_hex_float = true;
3058                            self.advance(); // consume '.'
3059                            if !self.advance_ascii_hex_digits() {
3060                                while !self.is_at_end() && self.peek().is_ascii_hexdigit() {
3061                                    self.advance();
3062                                }
3063                            }
3064                        }
3065                    }
3066                    // Optional binary exponent: p/P [+/-] digits
3067                    if has_hex_digits
3068                        && !self.is_at_end()
3069                        && (self.peek() == 'p' || self.peek() == 'P')
3070                    {
3071                        is_hex_float = true;
3072                        self.advance(); // consume p/P
3073                        if !self.is_at_end() && (self.peek() == '+' || self.peek() == '-') {
3074                            self.advance();
3075                        }
3076                        if !self.advance_ascii_digits() {
3077                            while !self.is_at_end() && self.peek().is_ascii_digit() {
3078                                self.advance();
3079                            }
3080                        }
3081                    }
3082                    if is_hex_float {
3083                        // Hex float literal — emit as regular Number token with full text
3084                        let raw_text = self.text_from_range(self.start, self.current);
3085                        let full_text = if self.config.numbers_can_be_underscore_separated
3086                            && raw_text.contains('_')
3087                        {
3088                            raw_text.replace('_', "")
3089                        } else {
3090                            raw_text
3091                        };
3092                        self.add_token_with_text(TokenType::Number, full_text);
3093                    } else if self.config.hex_string_is_integer_type {
3094                        // BigQuery/ClickHouse: 0xA represents an integer in hex notation
3095                        let raw_value = self.text_from_range(hex_start, self.current);
3096                        let hex_value = if self.config.numbers_can_be_underscore_separated
3097                            && raw_value.contains('_')
3098                        {
3099                            raw_value.replace('_', "")
3100                        } else {
3101                            raw_value
3102                        };
3103                        self.add_token_with_text(TokenType::HexNumber, hex_value);
3104                    } else {
3105                        // SQLite/Teradata: 0xCC represents a binary/blob hex string
3106                        let raw_value = self.text_from_range(hex_start, self.current);
3107                        let hex_value = if self.config.numbers_can_be_underscore_separated
3108                            && raw_value.contains('_')
3109                        {
3110                            raw_value.replace('_', "")
3111                        } else {
3112                            raw_value
3113                        };
3114                        self.add_token_with_text(TokenType::HexString, hex_value);
3115                    }
3116                    return Ok(());
3117                }
3118                // No hex digits after 0x - fall through to normal number parsing
3119                // (reset current back to after '0')
3120                self.current = self.start + 1;
3121            }
3122        }
3123
3124        // Allow underscores as digit separators (e.g., 20_000, 1_000_000)
3125        if !self.advance_ascii_digits() {
3126            while !self.is_at_end() && (self.peek().is_ascii_digit() || self.peek() == '_') {
3127                // Don't allow underscore at the end (must be followed by digit)
3128                if self.peek() == '_' && (self.is_at_end() || !self.peek_next().is_ascii_digit()) {
3129                    break;
3130                }
3131                self.advance();
3132            }
3133        }
3134
3135        // Look for decimal part - allow trailing dot (e.g., "1.")
3136        // In PostgreSQL (and sqlglot), "1.x" parses as float "1." with alias "x"
3137        // So we always consume the dot as part of the number, even if followed by an identifier
3138        if self.peek() == '.' {
3139            let next = self.peek_next();
3140            // Only consume the dot if:
3141            // 1. Followed by a digit (normal decimal like 1.5)
3142            // 2. Followed by an identifier start (like 1.x -> becomes 1. with alias x)
3143            // 3. End of input or other non-dot character (trailing decimal like "1.")
3144            // Do NOT consume if it's a double dot (..) which is a range operator
3145            if next != '.' {
3146                self.advance(); // consume the .
3147                                // Only consume digits after the decimal point (not identifiers)
3148                if !self.advance_ascii_digits() {
3149                    while !self.is_at_end() && (self.peek().is_ascii_digit() || self.peek() == '_')
3150                    {
3151                        if self.peek() == '_' && !self.peek_next().is_ascii_digit() {
3152                            break;
3153                        }
3154                        self.advance();
3155                    }
3156                }
3157            }
3158        }
3159
3160        // Look for exponent
3161        if self.peek() == 'e' || self.peek() == 'E' {
3162            self.advance();
3163            if self.peek() == '+' || self.peek() == '-' {
3164                self.advance();
3165            }
3166            if !self.advance_ascii_digits() {
3167                while !self.is_at_end() && (self.peek().is_ascii_digit() || self.peek() == '_') {
3168                    if self.peek() == '_' && !self.peek_next().is_ascii_digit() {
3169                        break;
3170                    }
3171                    self.advance();
3172                }
3173            }
3174        }
3175
3176        let source_text = self
3177            .cursor
3178            .source_range(self.source, self.start, self.current);
3179        let raw_owned = source_text
3180            .is_none()
3181            .then(|| self.text_from_range(self.start, self.current));
3182        let raw_text = source_text.unwrap_or_else(|| {
3183            raw_owned
3184                .as_deref()
3185                .expect("non-ASCII numbers own their text")
3186        });
3187        // Strip underscore digit separators (e.g., 20_000 -> 20000, 1_2E+1_0 -> 12E+10)
3188        // Only for dialects that support this (ClickHouse, DuckDB)
3189        let normalized = (self.config.numbers_can_be_underscore_separated
3190            && raw_text.contains('_'))
3191        .then(|| raw_text.replace('_', ""));
3192        let text = normalized.as_deref().unwrap_or(raw_text);
3193
3194        // Check for numeric literal suffixes (e.g., 1L -> BIGINT, 1s -> SMALLINT in Hive/Spark)
3195        if !self.config.numeric_literals.is_empty() && !self.is_at_end() {
3196            let next_char: String = self.peek().to_ascii_uppercase().to_string();
3197            // Try 2-char suffix first (e.g., "BD"), then 1-char
3198            let suffix_match = if self.current + 1 < self.size {
3199                let two_char: String = [
3200                    self.char_at(self.current).to_ascii_uppercase(),
3201                    self.char_at(self.current + 1).to_ascii_uppercase(),
3202                ]
3203                .iter()
3204                .collect();
3205                if self.config.numeric_literals.contains_key(&two_char) {
3206                    // Make sure the 2-char suffix is not followed by more identifier chars
3207                    let after_suffix = if self.current + 2 < self.size {
3208                        self.char_at(self.current + 2)
3209                    } else {
3210                        ' '
3211                    };
3212                    if !after_suffix.is_alphanumeric() && after_suffix != '_' {
3213                        Some((two_char, 2))
3214                    } else {
3215                        None
3216                    }
3217                } else if self.config.numeric_literals.contains_key(&next_char) {
3218                    // 1-char suffix - make sure not followed by more identifier chars
3219                    let after_suffix = if self.current + 1 < self.size {
3220                        self.char_at(self.current + 1)
3221                    } else {
3222                        ' '
3223                    };
3224                    if !after_suffix.is_alphanumeric() && after_suffix != '_' {
3225                        Some((next_char, 1))
3226                    } else {
3227                        None
3228                    }
3229                } else {
3230                    None
3231                }
3232            } else if self.config.numeric_literals.contains_key(&next_char) {
3233                // At end of input, 1-char suffix
3234                Some((next_char, 1))
3235            } else {
3236                None
3237            };
3238
3239            if let Some((suffix, len)) = suffix_match {
3240                // Consume the suffix characters
3241                for _ in 0..len {
3242                    self.advance();
3243                }
3244                // Emit as a special number-with-suffix token
3245                // We'll encode as "number::TYPE" so the parser can split it
3246                let type_name = self
3247                    .config
3248                    .numeric_literals
3249                    .get(&suffix)
3250                    .expect("suffix verified by contains_key above")
3251                    .clone();
3252                let combined = format!("{}::{}", text, type_name);
3253                self.add_token_with_text(TokenType::Number, combined);
3254                return Ok(());
3255            }
3256        }
3257
3258        // Check for identifiers that start with a digit (e.g., 1a, 1_a, 1a_1a)
3259        // In Hive/Spark/MySQL/ClickHouse, these are valid unquoted identifiers
3260        if self.config.identifiers_can_start_with_digit && !self.is_at_end() {
3261            let next = self.peek();
3262            if next.is_alphabetic() || next == '_' {
3263                // Continue scanning as an identifier
3264                if !self.advance_ascii_identifier() {
3265                    while !self.is_at_end() {
3266                        let ch = self.peek();
3267                        if ch.is_alphanumeric() || ch == '_' {
3268                            self.advance();
3269                        } else {
3270                            break;
3271                        }
3272                    }
3273                }
3274                self.add_token(TokenType::Identifier);
3275                return Ok(());
3276            }
3277        }
3278
3279        if let Some(text) = normalized.or(raw_owned) {
3280            self.add_token_with_text(TokenType::Number, text);
3281        } else {
3282            self.add_token(TokenType::Number);
3283        }
3284        Ok(())
3285    }
3286
3287    /// Scan a number that starts with a dot (e.g., .25, .5, .123e10)
3288    fn scan_number_starting_with_dot(&mut self) -> Result<()> {
3289        // Consume the leading dot
3290        self.advance();
3291
3292        // Consume the fractional digits
3293        if !self.advance_ascii_digits() {
3294            while !self.is_at_end() && (self.peek().is_ascii_digit() || self.peek() == '_') {
3295                if self.peek() == '_' && !self.peek_next().is_ascii_digit() {
3296                    break;
3297                }
3298                self.advance();
3299            }
3300        }
3301
3302        // Look for exponent
3303        if self.peek() == 'e' || self.peek() == 'E' {
3304            self.advance();
3305            if self.peek() == '+' || self.peek() == '-' {
3306                self.advance();
3307            }
3308            if !self.advance_ascii_digits() {
3309                while !self.is_at_end() && (self.peek().is_ascii_digit() || self.peek() == '_') {
3310                    if self.peek() == '_' && !self.peek_next().is_ascii_digit() {
3311                        break;
3312                    }
3313                    self.advance();
3314                }
3315            }
3316        }
3317
3318        let source_text = self
3319            .cursor
3320            .source_range(self.source, self.start, self.current);
3321        let raw_owned = source_text
3322            .is_none()
3323            .then(|| self.text_from_range(self.start, self.current));
3324        let raw_text = source_text.unwrap_or_else(|| {
3325            raw_owned
3326                .as_deref()
3327                .expect("non-ASCII numbers own their text")
3328        });
3329        // Strip underscore digit separators (e.g., .1_5 -> .15)
3330        // Only for dialects that support this (ClickHouse, DuckDB)
3331        let normalized = (self.config.numbers_can_be_underscore_separated
3332            && raw_text.contains('_'))
3333        .then(|| raw_text.replace('_', ""));
3334        if let Some(text) = normalized.or(raw_owned) {
3335            self.add_token_with_text(TokenType::Number, text);
3336        } else {
3337            self.add_token(TokenType::Number);
3338        }
3339        Ok(())
3340    }
3341
3342    /// Look up a keyword using a stack buffer for ASCII uppercasing, avoiding heap allocation.
3343    /// Returns `TokenType::Var` for texts longer than 128 bytes or non-UTF-8 results.
3344    #[inline]
3345    fn lookup_keyword_ascii(keywords: &HashMap<String, TokenType>, text: &str) -> TokenType {
3346        if text.len() > 128 {
3347            return TokenType::Var;
3348        }
3349        let mut buf = [0u8; 128];
3350        for (i, b) in text.bytes().enumerate() {
3351            buf[i] = b.to_ascii_uppercase();
3352        }
3353        if let Ok(upper) = std::str::from_utf8(&buf[..text.len()]) {
3354            keywords.get(upper).copied().unwrap_or(TokenType::Var)
3355        } else {
3356            TokenType::Var
3357        }
3358    }
3359
3360    fn scan_identifier_or_keyword(&mut self) -> Result<()> {
3361        // Guard against unrecognized characters that could cause infinite loops
3362        let first_char = self.peek();
3363        if !first_char.is_alphanumeric() && first_char != '_' {
3364            // Unknown character - skip it and return an error
3365            let c = self.advance();
3366            return Err(Error::tokenize(
3367                format!("Unexpected character: '{}'", c),
3368                self.line,
3369                self.column,
3370                self.start,
3371                self.current,
3372            ));
3373        }
3374
3375        if !self.advance_ascii_identifier() {
3376            while !self.is_at_end() {
3377                let c = self.peek();
3378                // Allow alphanumeric, underscore, $, # and @ in identifiers
3379                // PostgreSQL allows $, TSQL allows # and @
3380                // But stop consuming # if followed by > or >> (PostgreSQL #> and #>> operators)
3381                if c == '#' {
3382                    let next_c = if self.current + 1 < self.size {
3383                        self.char_at(self.current + 1)
3384                    } else {
3385                        '\0'
3386                    };
3387                    if next_c == '>' || next_c == '-' {
3388                        break; // Don't consume # — it's part of #>, #>>, or #- operator
3389                    }
3390                    self.advance();
3391                } else if c.is_alphanumeric() || c == '_' || c == '$' || c == '@' {
3392                    self.advance();
3393                } else {
3394                    break;
3395                }
3396            }
3397        }
3398
3399        let source_text = self
3400            .cursor
3401            .source_range(self.source, self.start, self.current);
3402        let owned_text = source_text
3403            .is_none()
3404            .then(|| self.text_from_range(self.start, self.current));
3405        let text = source_text.unwrap_or_else(|| {
3406            owned_text
3407                .as_deref()
3408                .expect("non-ASCII identifiers own their text")
3409        });
3410
3411        // Special-case NOT= (Teradata and other dialects)
3412        if text.eq_ignore_ascii_case("NOT") && self.peek() == '=' {
3413            self.advance(); // consume '='
3414            self.add_token(TokenType::Neq);
3415            return Ok(());
3416        }
3417
3418        // Check for special string prefixes like N'...', X'...', B'...', U&'...', r'...', b'...'
3419        // Also handle double-quoted variants for dialects that support them (e.g., BigQuery)
3420        let next_char = self.peek();
3421        let is_single_quote = next_char == '\'';
3422        let is_double_quote = next_char == '"' && self.config.quotes.contains_key("\"");
3423        // For raw strings (r"..." or r'...'), we allow double quotes even if " is not in quotes config
3424        // because raw strings are a special case used in Spark/Databricks where " is for identifiers
3425        let is_double_quote_for_raw = next_char == '"';
3426
3427        // Handle raw strings first - they're special because they work with both ' and "
3428        // even in dialects where " is normally an identifier delimiter (like Databricks)
3429        if text.eq_ignore_ascii_case("R") && (is_single_quote || is_double_quote_for_raw) {
3430            // Raw string r'...' or r"..." or r'''...''' or r"""...""" (BigQuery style)
3431            // In raw strings, backslashes are treated literally (no escape processing)
3432            let quote_char = if is_single_quote { '\'' } else { '"' };
3433            self.advance(); // consume the first opening quote
3434
3435            // Check for triple-quoted raw string (r"""...""" or r'''...''')
3436            if self.peek() == quote_char && self.peek_next() == quote_char {
3437                // Triple-quoted raw string
3438                self.advance(); // consume second quote
3439                self.advance(); // consume third quote
3440                let string_value = self.scan_raw_triple_quoted_content(quote_char)?;
3441                self.add_token_with_text(TokenType::RawString, string_value);
3442            } else {
3443                let string_value = self.scan_raw_string_content(quote_char)?;
3444                self.add_token_with_text(TokenType::RawString, string_value);
3445            }
3446            return Ok(());
3447        }
3448
3449        if is_single_quote || is_double_quote {
3450            if text.eq_ignore_ascii_case("N") {
3451                // National string N'...'
3452                self.advance(); // consume the opening quote
3453                let string_value = if is_single_quote {
3454                    self.scan_string_content()?
3455                } else {
3456                    self.scan_double_quoted_string_content()?
3457                };
3458                self.add_token_with_text(TokenType::NationalString, string_value);
3459                return Ok(());
3460            } else if text.eq_ignore_ascii_case("E") {
3461                // PostgreSQL escape string E'...' or e'...'
3462                // Preserve the case by prefixing with "e:" or "E:"
3463                // Always use backslash escapes for escape strings (e.g., \' is an escaped quote)
3464                let lowercase = text == "e";
3465                let prefix = if lowercase { "e:" } else { "E:" };
3466                self.advance(); // consume the opening quote
3467                let string_value = self.scan_string_content_with_escapes(true)?;
3468                self.add_token_with_text(
3469                    TokenType::EscapeString,
3470                    format!("{}{}", prefix, string_value),
3471                );
3472                return Ok(());
3473            } else if text.eq_ignore_ascii_case("X") {
3474                // Hex string X'...'
3475                self.advance(); // consume the opening quote
3476                let string_value = if is_single_quote {
3477                    self.scan_string_content()?
3478                } else {
3479                    self.scan_double_quoted_string_content()?
3480                };
3481                self.add_token_with_text(TokenType::HexString, string_value);
3482                return Ok(());
3483            } else if text.eq_ignore_ascii_case("B") && is_double_quote {
3484                // Byte string b"..." (BigQuery style) - MUST check before single quote B'...'
3485                self.advance(); // consume the opening quote
3486                let string_value = self.scan_double_quoted_string_content()?;
3487                self.add_token_with_text(TokenType::ByteString, string_value);
3488                return Ok(());
3489            } else if text.eq_ignore_ascii_case("B") && is_single_quote {
3490                // For BigQuery: b'...' is a byte string (bytes data)
3491                // For standard SQL: B'...' is a bit string (binary digits)
3492                self.advance(); // consume the opening quote
3493                let string_value = self.scan_string_content()?;
3494                if self.config.b_prefix_is_byte_string {
3495                    self.add_token_with_text(TokenType::ByteString, string_value);
3496                } else {
3497                    self.add_token_with_text(TokenType::BitString, string_value);
3498                }
3499                return Ok(());
3500            }
3501        }
3502
3503        // Check for U&'...' Unicode string syntax (SQL standard)
3504        if text.eq_ignore_ascii_case("U")
3505            && self.peek() == '&'
3506            && self.current + 1 < self.size
3507            && self.char_at(self.current + 1) == '\''
3508        {
3509            self.advance(); // consume '&'
3510            self.advance(); // consume opening quote
3511            let string_value = self.scan_string_content()?;
3512            self.add_token_with_text(TokenType::UnicodeString, string_value);
3513            return Ok(());
3514        }
3515
3516        let token_type = Self::lookup_keyword_ascii(&self.config.keywords, &text);
3517
3518        if let Some(text) = owned_text {
3519            self.add_token_with_text(token_type, text);
3520        } else {
3521            self.add_token_from_source(token_type, self.start, self.current);
3522        }
3523        Ok(())
3524    }
3525
3526    /// Scan string content (everything between quotes)
3527    /// If `force_backslash_escapes` is true, backslash is always treated as an escape character
3528    /// (used for PostgreSQL E'...' escape strings)
3529    fn scan_string_content_with_escapes(
3530        &mut self,
3531        force_backslash_escapes: bool,
3532    ) -> Result<String> {
3533        let use_backslash_escapes =
3534            force_backslash_escapes || self.config.string_escapes.contains(&'\\');
3535        if let Some((start, end)) = self.try_scan_simple_quoted_content('\'', use_backslash_escapes)
3536        {
3537            return Ok(self.text_from_range(start, end));
3538        }
3539        let mut value = String::new();
3540
3541        while !self.is_at_end() {
3542            let c = self.peek();
3543            if c == '\'' {
3544                if self.peek_next() == '\'' {
3545                    // Escaped quote ''
3546                    value.push('\'');
3547                    self.advance();
3548                    self.advance();
3549                } else {
3550                    break;
3551                }
3552            } else if c == '\\' && use_backslash_escapes {
3553                // Preserve escape sequences literally (including \' for escape strings)
3554                value.push(self.advance());
3555                if !self.is_at_end() {
3556                    value.push(self.advance());
3557                }
3558            } else {
3559                value.push(self.advance());
3560            }
3561        }
3562
3563        if self.is_at_end() {
3564            return Err(Error::tokenize(
3565                "Unterminated string",
3566                self.line,
3567                self.column,
3568                self.start,
3569                self.current,
3570            ));
3571        }
3572
3573        self.advance(); // Closing quote
3574        Ok(value)
3575    }
3576
3577    /// Scan string content (everything between quotes)
3578    fn scan_string_content(&mut self) -> Result<String> {
3579        self.scan_string_content_with_escapes(false)
3580    }
3581
3582    /// Scan double-quoted string content (for dialects like BigQuery where " is a string delimiter)
3583    /// This is used for prefixed strings like b"..." or N"..."
3584    fn scan_double_quoted_string_content(&mut self) -> Result<String> {
3585        let use_backslash_escapes = self.config.string_escapes.contains(&'\\');
3586        if let Some((start, end)) = self.try_scan_simple_quoted_content('"', use_backslash_escapes)
3587        {
3588            return Ok(self.text_from_range(start, end));
3589        }
3590        let mut value = String::new();
3591
3592        while !self.is_at_end() {
3593            let c = self.peek();
3594            if c == '"' {
3595                if self.peek_next() == '"' {
3596                    // Escaped quote ""
3597                    value.push('"');
3598                    self.advance();
3599                    self.advance();
3600                } else {
3601                    break;
3602                }
3603            } else if c == '\\' && use_backslash_escapes {
3604                // Handle escape sequences
3605                self.advance(); // Consume backslash
3606                if !self.is_at_end() {
3607                    let escaped = self.advance();
3608                    match escaped {
3609                        'n' => value.push('\n'),
3610                        'r' => value.push('\r'),
3611                        't' => value.push('\t'),
3612                        '0' => value.push('\0'),
3613                        '\\' => value.push('\\'),
3614                        '"' => value.push('"'),
3615                        '\'' => value.push('\''),
3616                        'x' => {
3617                            // Hex escape \xNN - collect hex digits
3618                            let mut hex = String::new();
3619                            for _ in 0..2 {
3620                                if !self.is_at_end() && self.peek().is_ascii_hexdigit() {
3621                                    hex.push(self.advance());
3622                                }
3623                            }
3624                            if let Ok(byte) = u8::from_str_radix(&hex, 16) {
3625                                value.push(byte as char);
3626                            } else {
3627                                // Invalid hex escape, keep it literal
3628                                value.push('\\');
3629                                value.push('x');
3630                                value.push_str(&hex);
3631                            }
3632                        }
3633                        _ => {
3634                            // For unrecognized escapes, preserve backslash + char
3635                            value.push('\\');
3636                            value.push(escaped);
3637                        }
3638                    }
3639                }
3640            } else {
3641                value.push(self.advance());
3642            }
3643        }
3644
3645        if self.is_at_end() {
3646            return Err(Error::tokenize(
3647                "Unterminated double-quoted string",
3648                self.line,
3649                self.column,
3650                self.start,
3651                self.current,
3652            ));
3653        }
3654
3655        self.advance(); // Closing quote
3656        Ok(value)
3657    }
3658
3659    /// Scan raw string content (limited escape processing for quotes)
3660    /// Used for BigQuery r'...' and r"..." strings
3661    /// In raw strings, backslashes are literal EXCEPT that escape sequences for the
3662    /// quote character still work (e.g., \' in r'...' escapes the quote, '' also works)
3663    fn scan_raw_string_content(&mut self, quote_char: char) -> Result<String> {
3664        if let Some((start, end)) = self.try_scan_simple_quoted_content(
3665            quote_char,
3666            self.config.string_escapes_allowed_in_raw_strings,
3667        ) {
3668            return Ok(self.text_from_range(start, end));
3669        }
3670        let mut value = String::new();
3671
3672        while !self.is_at_end() {
3673            let c = self.peek();
3674            if c == quote_char {
3675                if self.peek_next() == quote_char {
3676                    // Escaped quote (doubled) - e.g., '' inside r'...'
3677                    value.push(quote_char);
3678                    self.advance();
3679                    self.advance();
3680                } else {
3681                    break;
3682                }
3683            } else if c == '\\'
3684                && self.peek_next() == quote_char
3685                && self.config.string_escapes_allowed_in_raw_strings
3686            {
3687                // The quote does not terminate the raw string, but both characters
3688                // remain literal content.
3689                value.push('\\');
3690                value.push(quote_char);
3691                self.advance(); // consume backslash
3692                self.advance(); // consume quote
3693            } else {
3694                // In raw strings, everything including backslashes is literal
3695                value.push(self.advance());
3696            }
3697        }
3698
3699        if self.is_at_end() {
3700            return Err(Error::tokenize(
3701                "Unterminated raw string",
3702                self.line,
3703                self.column,
3704                self.start,
3705                self.current,
3706            ));
3707        }
3708
3709        self.advance(); // Closing quote
3710        Ok(value)
3711    }
3712
3713    /// Scan raw triple-quoted string content (r"""...""" or r'''...''')
3714    /// Terminates when three consecutive quote_chars are found
3715    fn scan_raw_triple_quoted_content(&mut self, quote_char: char) -> Result<String> {
3716        let mut value = String::new();
3717
3718        while !self.is_at_end() {
3719            if self.peek() == quote_char {
3720                let mut quote_count = 0;
3721                while self.current + quote_count < self.size
3722                    && self.char_at(self.current + quote_count) == quote_char
3723                {
3724                    quote_count += 1;
3725                }
3726                if quote_count >= 3 {
3727                    // When more than three quotes occur, the leading quotes are content
3728                    // and the final three terminate the raw triple-quoted string.
3729                    for _ in 0..quote_count - 3 {
3730                        value.push(quote_char);
3731                    }
3732                    for _ in 0..quote_count {
3733                        self.advance();
3734                    }
3735                    return Ok(value);
3736                }
3737            }
3738            // In raw strings, everything including backslashes is literal
3739            let ch = self.advance();
3740            value.push(ch);
3741        }
3742
3743        Err(Error::tokenize(
3744            "Unterminated raw triple-quoted string",
3745            self.line,
3746            self.column,
3747            self.start,
3748            self.current,
3749        ))
3750    }
3751
3752    /// Scan TSQL identifiers that start with # (temp tables) or @ (variables)
3753    /// Examples: #temp, ##global_temp, @variable
3754    /// Scan an identifier that starts with `$` (ClickHouse).
3755    /// Examples: `$alias$name$`, `$x`
3756    fn scan_dollar_identifier(&mut self) -> Result<()> {
3757        // Consume the leading $
3758        self.advance();
3759
3760        // Consume alphanumeric, _, and $ continuation chars
3761        while !self.is_at_end() {
3762            let c = self.peek();
3763            if c.is_alphanumeric() || c == '_' || c == '$' {
3764                self.advance();
3765            } else {
3766                break;
3767            }
3768        }
3769
3770        self.add_token(TokenType::Var);
3771        Ok(())
3772    }
3773
3774    fn scan_tsql_identifier(&mut self) -> Result<()> {
3775        // Consume the leading # or @ (or ##)
3776        let first = self.advance();
3777
3778        // For ##, consume the second #
3779        if first == '#' && self.peek() == '#' {
3780            self.advance();
3781        }
3782
3783        // Now scan the rest of the identifier
3784        if !self.advance_ascii_identifier() {
3785            while !self.is_at_end() {
3786                let c = self.peek();
3787                if c.is_alphanumeric() || c == '_' || c == '$' || c == '#' || c == '@' {
3788                    self.advance();
3789                } else {
3790                    break;
3791                }
3792            }
3793        }
3794
3795        // These are always identifiers (variables or temp table names), never keywords
3796        self.add_token(TokenType::Var);
3797        Ok(())
3798    }
3799
3800    /// Check if the last tokens match INSERT ... FORMAT <name> (not VALUES).
3801    /// If so, consume everything until the next blank line (two consecutive newlines)
3802    /// or end of input as raw data.
3803    fn try_scan_insert_format_raw_data(&mut self) -> Option<String> {
3804        let len = self.tokens.len();
3805        if len < 3 {
3806            return None;
3807        }
3808
3809        // Last token should be the format name (Identifier or Var, not VALUES)
3810        let last = &self.tokens[len - 1];
3811        if last.text(self.source).eq_ignore_ascii_case("VALUES") {
3812            return None;
3813        }
3814        if !matches!(last.token_type(), TokenType::Var | TokenType::Identifier) {
3815            return None;
3816        }
3817
3818        // Second-to-last should be FORMAT
3819        let format_tok = &self.tokens[len - 2];
3820        if !format_tok.text(self.source).eq_ignore_ascii_case("FORMAT") {
3821            return None;
3822        }
3823
3824        // Check that there's an INSERT somewhere earlier in the tokens
3825        let has_insert = self.tokens[..len - 2]
3826            .iter()
3827            .rev()
3828            .take(20)
3829            .any(|t| t.token_type() == TokenType::Insert);
3830        if !has_insert {
3831            return None;
3832        }
3833
3834        // We're in INSERT ... FORMAT <name> context. Consume everything until:
3835        // - A blank line (two consecutive newlines, possibly with whitespace between)
3836        // - End of input
3837        let raw_start = self.current;
3838        while !self.is_at_end() {
3839            let c = self.peek();
3840            if c == '\n' {
3841                // Check for blank line: \n followed by optional \r and \n
3842                let saved = self.current;
3843                self.advance(); // consume first \n
3844                                // Skip \r if present
3845                while !self.is_at_end() && self.peek() == '\r' {
3846                    self.advance();
3847                }
3848                if self.is_at_end() || self.peek() == '\n' {
3849                    // Found blank line or end of input - stop here
3850                    // Don't consume the second \n so subsequent SQL can be tokenized
3851                    let raw = self.text_from_range(raw_start, saved);
3852                    return Some(raw.trim().to_string());
3853                }
3854                // Not a blank line, continue scanning
3855            } else {
3856                self.advance();
3857            }
3858        }
3859
3860        // Reached end of input
3861        let raw = self.text_from_range(raw_start, self.current);
3862        let trimmed = raw.trim().to_string();
3863        if trimmed.is_empty() {
3864            None
3865        } else {
3866            Some(trimmed)
3867        }
3868    }
3869
3870    fn add_token(&mut self, token_type: TokenType) {
3871        self.add_token_from_source(token_type, self.start, self.current);
3872    }
3873
3874    fn add_token_from_source(&mut self, token_type: TokenType, text_start: usize, text_end: usize) {
3875        let span = Span::new(self.start, self.current, self.line, self.column);
3876        if let Some(stats) = &mut self.guard_stats {
3877            stats.observe(token_type, span);
3878        }
3879        let mut token = if self
3880            .cursor
3881            .source_range(self.source, text_start, text_end)
3882            .is_some()
3883        {
3884            T::from_source(
3885                token_type,
3886                self.source,
3887                text_start,
3888                text_end,
3889                span,
3890                self.shared_source.as_ref(),
3891            )
3892        } else {
3893            T::from_owned(
3894                token_type,
3895                self.cursor
3896                    .text_from_range(self.source, text_start, text_end),
3897                span,
3898            )
3899        };
3900        token.comments_mut().append(&mut self.comments);
3901        self.tokens.push(token);
3902    }
3903
3904    fn add_token_with_text(&mut self, token_type: TokenType, text: String) {
3905        let span = Span::new(self.start, self.current, self.line, self.column);
3906        if let Some(stats) = &mut self.guard_stats {
3907            stats.observe(token_type, span);
3908        }
3909        let mut token = T::from_owned(token_type, text, span);
3910        token.comments_mut().append(&mut self.comments);
3911        self.tokens.push(token);
3912    }
3913}
3914
3915#[cfg(test)]
3916mod tests {
3917    use super::*;
3918
3919    #[test]
3920    fn ascii_fast_path_matches_character_buffer_path() {
3921        let tokenizer = Tokenizer::default();
3922        let inputs = [
3923            "SELECT a, b FROM t WHERE id IN (1, 2, 3)",
3924            "SELECT 'it''s', \"quoted\", $1 /* comment */ FROM schema.table",
3925            "INSERT INTO t VALUES (1, 'a'), (2, 'b'); UPDATE t SET value = 'c'",
3926            "SELECT $$body$$, $tag$content$tag$, 0xFF, 1.25e-2",
3927        ];
3928
3929        for sql in inputs {
3930            assert_eq!(
3931                tokenizer.tokenize(sql).unwrap(),
3932                tokenizer.tokenize_without_ascii_fast_path(sql).unwrap(),
3933                "tokenization differs for {sql}"
3934            );
3935        }
3936    }
3937
3938    #[test]
3939    fn parser_tokens_match_public_tokens() {
3940        let tokenizer = Tokenizer::default();
3941        let inputs = [
3942            "SELECT alpha, 123, 'plain' FROM schema.table WHERE id = 42",
3943            "SELECT 'it''s', $$body$$, $tag$content$tag$ /* comment */",
3944            "SELECT cafe, 'naive' FROM t\nWHERE value >= 1.25e-2",
3945            "SELECT cafe, 'caf\u{e9}', \u{3b4}elta FROM donn\u{e9}es",
3946        ];
3947
3948        for sql in inputs {
3949            let public = tokenizer.tokenize(sql).unwrap();
3950            let source: Arc<str> = Arc::from(sql);
3951            let (parser, stats) = tokenizer.tokenize_for_parser(&source).unwrap();
3952            let materialized = parser
3953                .iter()
3954                .map(|token| Token {
3955                    token_type: token.token_type,
3956                    text: token.text_owned(),
3957                    span: token.span,
3958                    comments: token.comments.clone(),
3959                    trailing_comments: token.trailing_comments.clone(),
3960                })
3961                .collect::<Vec<_>>();
3962
3963            assert_eq!(
3964                materialized, public,
3965                "parser tokenization differs for {sql}"
3966            );
3967            assert_eq!(stats.token_count, public.len());
3968        }
3969    }
3970
3971    #[test]
3972    fn parser_tokens_borrow_unchanged_ascii_text() {
3973        let tokenizer = Tokenizer::default();
3974        let source: Arc<str> = Arc::from("SELECT alpha, 123, 'plain'");
3975        let (tokens, _) = tokenizer.tokenize_for_parser(&source).unwrap();
3976
3977        assert!(tokens
3978            .iter()
3979            .all(|token| matches!(&token.text, ParserTokenText::Source { .. })));
3980    }
3981
3982    #[test]
3983    fn test_simple_select() {
3984        let tokenizer = Tokenizer::default();
3985        let tokens = tokenizer.tokenize("SELECT 1").unwrap();
3986
3987        assert_eq!(tokens.len(), 2);
3988        assert_eq!(tokens[0].token_type, TokenType::Select);
3989        assert_eq!(tokens[1].token_type, TokenType::Number);
3990        assert_eq!(tokens[1].text, "1");
3991    }
3992
3993    #[test]
3994    fn test_select_with_identifier() {
3995        let tokenizer = Tokenizer::default();
3996        let tokens = tokenizer.tokenize("SELECT a, b FROM t").unwrap();
3997
3998        assert_eq!(tokens.len(), 6);
3999        assert_eq!(tokens[0].token_type, TokenType::Select);
4000        assert_eq!(tokens[1].token_type, TokenType::Var);
4001        assert_eq!(tokens[1].text, "a");
4002        assert_eq!(tokens[2].token_type, TokenType::Comma);
4003        assert_eq!(tokens[3].token_type, TokenType::Var);
4004        assert_eq!(tokens[3].text, "b");
4005        assert_eq!(tokens[4].token_type, TokenType::From);
4006        assert_eq!(tokens[5].token_type, TokenType::Var);
4007        assert_eq!(tokens[5].text, "t");
4008    }
4009
4010    #[test]
4011    fn test_string_literal() {
4012        let tokenizer = Tokenizer::default();
4013        let tokens = tokenizer.tokenize("SELECT 'hello'").unwrap();
4014
4015        assert_eq!(tokens.len(), 2);
4016        assert_eq!(tokens[1].token_type, TokenType::String);
4017        assert_eq!(tokens[1].text, "hello");
4018    }
4019
4020    #[test]
4021    fn test_escaped_string() {
4022        let tokenizer = Tokenizer::default();
4023        let tokens = tokenizer.tokenize("SELECT 'it''s'").unwrap();
4024
4025        assert_eq!(tokens.len(), 2);
4026        assert_eq!(tokens[1].token_type, TokenType::String);
4027        assert_eq!(tokens[1].text, "it's");
4028    }
4029
4030    #[test]
4031    fn test_escape_follow_chars_gate_builtin_decoding() {
4032        let mut config = TokenizerConfig::default();
4033        config.string_escapes.push('\\');
4034        config.escape_follow_chars = vec!['n'];
4035        let tokenizer = Tokenizer::new(config);
4036        let tokens = tokenizer.tokenize(r"SELECT '\n\a\f\Z\x21'").unwrap();
4037
4038        assert_eq!(tokens[1].text, "\nafZx21");
4039    }
4040
4041    #[test]
4042    fn test_configured_numeric_escapes_require_complete_sequences() {
4043        let mut config = TokenizerConfig::default();
4044        config.string_escapes.push('\\');
4045        config.escape_follow_chars = vec!['0', '1', '2', '3', '4', '5', '6', '7', 'x', 'u'];
4046        let tokenizer = Tokenizer::new(config);
4047        let tokens = tokenizer
4048            .tokenize(r"SELECT '\041\x21\u26c4-\777\x2\u26c'")
4049            .unwrap();
4050
4051        assert_eq!(tokens[1].text, "!!\u{26c4}-777x2u26c");
4052    }
4053
4054    #[test]
4055    fn test_terminal_backslash_quote_recovery() {
4056        let mut config = TokenizerConfig::default();
4057        config.string_escapes.push('\\');
4058        config.recover_terminal_backslash_quote = true;
4059        let tokenizer = Tokenizer::new(config);
4060        let tokens = tokenizer
4061            .tokenize("SHOW FUNCTIONS LIKE 'a\\' OR 1=1")
4062            .unwrap();
4063
4064        assert_eq!(tokens.len(), 8);
4065        assert_eq!(tokens[3].token_type, TokenType::String);
4066        assert_eq!(tokens[3].text, "a\\");
4067        assert_eq!(tokens[4].token_type, TokenType::Or);
4068    }
4069
4070    #[test]
4071    fn test_comments() {
4072        let tokenizer = Tokenizer::default();
4073        let tokens = tokenizer.tokenize("SELECT -- comment\n1").unwrap();
4074
4075        assert_eq!(tokens.len(), 2);
4076        // Comments are attached to the PREVIOUS token as trailing_comments
4077        // This is better for round-trip fidelity (e.g., SELECT c /* comment */ FROM)
4078        assert_eq!(tokens[0].trailing_comments.len(), 1);
4079        assert_eq!(tokens[0].trailing_comments[0], " comment");
4080    }
4081
4082    #[test]
4083    fn test_comment_in_and_chain() {
4084        use crate::generator::Generator;
4085        use crate::parser::Parser;
4086
4087        // Line comments between AND clauses should appear after the AND operator
4088        let sql = "SELECT a FROM b WHERE foo\n-- c1\nAND bar\n-- c2\nAND bla";
4089        let ast = Parser::parse_sql(sql).unwrap();
4090        let mut gen = Generator::default();
4091        let output = gen.generate(&ast[0]).unwrap();
4092        assert_eq!(
4093            output,
4094            "SELECT a FROM b WHERE foo AND /* c1 */ bar AND /* c2 */ bla"
4095        );
4096    }
4097
4098    #[test]
4099    fn test_operators() {
4100        let tokenizer = Tokenizer::default();
4101        let tokens = tokenizer.tokenize("1 + 2 * 3").unwrap();
4102
4103        assert_eq!(tokens.len(), 5);
4104        assert_eq!(tokens[0].token_type, TokenType::Number);
4105        assert_eq!(tokens[1].token_type, TokenType::Plus);
4106        assert_eq!(tokens[2].token_type, TokenType::Number);
4107        assert_eq!(tokens[3].token_type, TokenType::Star);
4108        assert_eq!(tokens[4].token_type, TokenType::Number);
4109    }
4110
4111    #[test]
4112    fn test_comparison_operators() {
4113        let tokenizer = Tokenizer::default();
4114        let tokens = tokenizer.tokenize("a <= b >= c != d").unwrap();
4115
4116        assert_eq!(tokens[1].token_type, TokenType::Lte);
4117        assert_eq!(tokens[3].token_type, TokenType::Gte);
4118        assert_eq!(tokens[5].token_type, TokenType::Neq);
4119    }
4120
4121    #[test]
4122    fn test_national_string() {
4123        let tokenizer = Tokenizer::default();
4124        let tokens = tokenizer.tokenize("N'abc'").unwrap();
4125
4126        assert_eq!(
4127            tokens.len(),
4128            1,
4129            "Expected 1 token for N'abc', got {:?}",
4130            tokens
4131        );
4132        assert_eq!(tokens[0].token_type, TokenType::NationalString);
4133        assert_eq!(tokens[0].text, "abc");
4134    }
4135
4136    #[test]
4137    fn test_hex_string() {
4138        let tokenizer = Tokenizer::default();
4139        let tokens = tokenizer.tokenize("X'ABCD'").unwrap();
4140
4141        assert_eq!(
4142            tokens.len(),
4143            1,
4144            "Expected 1 token for X'ABCD', got {:?}",
4145            tokens
4146        );
4147        assert_eq!(tokens[0].token_type, TokenType::HexString);
4148        assert_eq!(tokens[0].text, "ABCD");
4149    }
4150
4151    #[test]
4152    fn test_bit_string() {
4153        let tokenizer = Tokenizer::default();
4154        let tokens = tokenizer.tokenize("B'01010'").unwrap();
4155
4156        assert_eq!(
4157            tokens.len(),
4158            1,
4159            "Expected 1 token for B'01010', got {:?}",
4160            tokens
4161        );
4162        assert_eq!(tokens[0].token_type, TokenType::BitString);
4163        assert_eq!(tokens[0].text, "01010");
4164    }
4165
4166    #[test]
4167    fn test_trailing_dot_number() {
4168        let tokenizer = Tokenizer::default();
4169
4170        // Test trailing dot
4171        let tokens = tokenizer.tokenize("SELECT 1.").unwrap();
4172        assert_eq!(
4173            tokens.len(),
4174            2,
4175            "Expected 2 tokens for 'SELECT 1.', got {:?}",
4176            tokens
4177        );
4178        assert_eq!(tokens[1].token_type, TokenType::Number);
4179        assert_eq!(tokens[1].text, "1.");
4180
4181        // Test normal decimal
4182        let tokens = tokenizer.tokenize("SELECT 1.5").unwrap();
4183        assert_eq!(tokens[1].text, "1.5");
4184
4185        // Test number followed by dot and identifier
4186        // In PostgreSQL (and sqlglot), "1.x" parses as float "1." with alias "x"
4187        let tokens = tokenizer.tokenize("SELECT 1.a").unwrap();
4188        assert_eq!(
4189            tokens.len(),
4190            3,
4191            "Expected 3 tokens for 'SELECT 1.a', got {:?}",
4192            tokens
4193        );
4194        assert_eq!(tokens[1].token_type, TokenType::Number);
4195        assert_eq!(tokens[1].text, "1.");
4196        assert_eq!(tokens[2].token_type, TokenType::Var);
4197
4198        // Test two dots (range operator) - dot is NOT consumed when followed by another dot
4199        let tokens = tokenizer.tokenize("SELECT 1..2").unwrap();
4200        assert_eq!(tokens[1].token_type, TokenType::Number);
4201        assert_eq!(tokens[1].text, "1");
4202        assert_eq!(tokens[2].token_type, TokenType::Dot);
4203        assert_eq!(tokens[3].token_type, TokenType::Dot);
4204        assert_eq!(tokens[4].token_type, TokenType::Number);
4205        assert_eq!(tokens[4].text, "2");
4206    }
4207
4208    #[test]
4209    fn test_leading_dot_number() {
4210        let tokenizer = Tokenizer::default();
4211
4212        // Test leading dot number (e.g., .25 for 0.25)
4213        let tokens = tokenizer.tokenize(".25").unwrap();
4214        assert_eq!(
4215            tokens.len(),
4216            1,
4217            "Expected 1 token for '.25', got {:?}",
4218            tokens
4219        );
4220        assert_eq!(tokens[0].token_type, TokenType::Number);
4221        assert_eq!(tokens[0].text, ".25");
4222
4223        // Test leading dot in context (Oracle SAMPLE clause)
4224        let tokens = tokenizer.tokenize("SAMPLE (.25)").unwrap();
4225        assert_eq!(
4226            tokens.len(),
4227            4,
4228            "Expected 4 tokens for 'SAMPLE (.25)', got {:?}",
4229            tokens
4230        );
4231        assert_eq!(tokens[0].token_type, TokenType::Sample);
4232        assert_eq!(tokens[1].token_type, TokenType::LParen);
4233        assert_eq!(tokens[2].token_type, TokenType::Number);
4234        assert_eq!(tokens[2].text, ".25");
4235        assert_eq!(tokens[3].token_type, TokenType::RParen);
4236
4237        // Test leading dot with exponent
4238        let tokens = tokenizer.tokenize(".5e10").unwrap();
4239        assert_eq!(
4240            tokens.len(),
4241            1,
4242            "Expected 1 token for '.5e10', got {:?}",
4243            tokens
4244        );
4245        assert_eq!(tokens[0].token_type, TokenType::Number);
4246        assert_eq!(tokens[0].text, ".5e10");
4247
4248        // Test that plain dot is still a Dot token
4249        let tokens = tokenizer.tokenize("a.b").unwrap();
4250        assert_eq!(
4251            tokens.len(),
4252            3,
4253            "Expected 3 tokens for 'a.b', got {:?}",
4254            tokens
4255        );
4256        assert_eq!(tokens[1].token_type, TokenType::Dot);
4257    }
4258
4259    #[test]
4260    fn test_unrecognized_character() {
4261        let tokenizer = Tokenizer::default();
4262
4263        // Unicode curly quotes are now handled as string delimiters
4264        let result = tokenizer.tokenize("SELECT \u{2018}hello\u{2019}");
4265        assert!(
4266            result.is_ok(),
4267            "Curly quotes should be tokenized as strings"
4268        );
4269
4270        // Unicode bullet character should still error
4271        let result = tokenizer.tokenize("SELECT • FROM t");
4272        assert!(result.is_err());
4273    }
4274
4275    #[test]
4276    fn test_colon_eq_tokenization() {
4277        let tokenizer = Tokenizer::default();
4278
4279        // := should be a single ColonEq token
4280        let tokens = tokenizer.tokenize("a := 1").unwrap();
4281        assert_eq!(tokens.len(), 3);
4282        assert_eq!(tokens[0].token_type, TokenType::Var);
4283        assert_eq!(tokens[1].token_type, TokenType::ColonEq);
4284        assert_eq!(tokens[2].token_type, TokenType::Number);
4285
4286        // : followed by non-= should still be Colon
4287        let tokens = tokenizer.tokenize("a:b").unwrap();
4288        assert!(tokens.iter().any(|t| t.token_type == TokenType::Colon));
4289        assert!(!tokens.iter().any(|t| t.token_type == TokenType::ColonEq));
4290
4291        // :: should still be DColon
4292        let tokens = tokenizer.tokenize("a::INT").unwrap();
4293        assert!(tokens.iter().any(|t| t.token_type == TokenType::DColon));
4294    }
4295
4296    #[test]
4297    fn test_colon_eq_parsing() {
4298        use crate::generator::Generator;
4299        use crate::parser::Parser;
4300
4301        // MySQL @var := value in SELECT
4302        let ast = Parser::parse_sql("SELECT @var1 := 1, @var2")
4303            .expect("Failed to parse MySQL @var := expr");
4304        let output = Generator::sql(&ast[0]).expect("Failed to generate");
4305        assert_eq!(output, "SELECT @var1 := 1, @var2");
4306
4307        // MySQL @var := @var in SELECT
4308        let ast = Parser::parse_sql("SELECT @var1, @var2 := @var1")
4309            .expect("Failed to parse MySQL @var2 := @var1");
4310        let output = Generator::sql(&ast[0]).expect("Failed to generate");
4311        assert_eq!(output, "SELECT @var1, @var2 := @var1");
4312
4313        // MySQL @var := COUNT(*)
4314        let ast = Parser::parse_sql("SELECT @var1 := COUNT(*) FROM t1")
4315            .expect("Failed to parse MySQL @var := COUNT(*)");
4316        let output = Generator::sql(&ast[0]).expect("Failed to generate");
4317        assert_eq!(output, "SELECT @var1 := COUNT(*) FROM t1");
4318
4319        // MySQL SET @var := 1 (should normalize to = in output)
4320        let ast = Parser::parse_sql("SET @var1 := 1").expect("Failed to parse SET @var1 := 1");
4321        let output = Generator::sql(&ast[0]).expect("Failed to generate");
4322        assert_eq!(output, "SET @var1 = 1");
4323
4324        // Function named args with :=
4325        let ast =
4326            Parser::parse_sql("UNION_VALUE(k1 := 1)").expect("Failed to parse named arg with :=");
4327        let output = Generator::sql(&ast[0]).expect("Failed to generate");
4328        assert_eq!(output, "UNION_VALUE(k1 := 1)");
4329
4330        // UNNEST with recursive := TRUE
4331        let ast = Parser::parse_sql("SELECT UNNEST(col, recursive := TRUE) FROM t")
4332            .expect("Failed to parse UNNEST with :=");
4333        let output = Generator::sql(&ast[0]).expect("Failed to generate");
4334        assert_eq!(output, "SELECT UNNEST(col, recursive := TRUE) FROM t");
4335
4336        // DuckDB prefix alias: foo: 1 means 1 AS foo
4337        let ast =
4338            Parser::parse_sql("SELECT foo: 1").expect("Failed to parse DuckDB prefix alias foo: 1");
4339        let output = Generator::sql(&ast[0]).expect("Failed to generate");
4340        assert_eq!(output, "SELECT 1 AS foo");
4341
4342        // DuckDB prefix alias with multiple columns
4343        let ast = Parser::parse_sql("SELECT foo: 1, bar: 2, baz: 3")
4344            .expect("Failed to parse DuckDB multiple prefix aliases");
4345        let output = Generator::sql(&ast[0]).expect("Failed to generate");
4346        assert_eq!(output, "SELECT 1 AS foo, 2 AS bar, 3 AS baz");
4347    }
4348
4349    #[test]
4350    fn test_colon_eq_dialect_roundtrip() {
4351        use crate::dialects::{Dialect, DialectType};
4352
4353        fn check(dialect: DialectType, sql: &str, expected: Option<&str>) {
4354            let d = Dialect::get(dialect);
4355            let ast = d
4356                .parse(sql)
4357                .unwrap_or_else(|e| panic!("Parse error for '{}': {}", sql, e));
4358            assert!(!ast.is_empty(), "Empty AST for: {}", sql);
4359            let transformed = d
4360                .transform(ast[0].clone())
4361                .unwrap_or_else(|e| panic!("Transform error for '{}': {}", sql, e));
4362            let output = d
4363                .generate(&transformed)
4364                .unwrap_or_else(|e| panic!("Generate error for '{}': {}", sql, e));
4365            let expected = expected.unwrap_or(sql);
4366            assert_eq!(output, expected, "Roundtrip failed for: {}", sql);
4367        }
4368
4369        // MySQL := tests
4370        check(DialectType::MySQL, "SELECT @var1 := 1, @var2", None);
4371        check(DialectType::MySQL, "SELECT @var1, @var2 := @var1", None);
4372        check(DialectType::MySQL, "SELECT @var1 := COUNT(*) FROM t1", None);
4373        check(DialectType::MySQL, "SET @var1 := 1", Some("SET @var1 = 1"));
4374
4375        // DuckDB := tests
4376        check(
4377            DialectType::DuckDB,
4378            "SELECT UNNEST(col, recursive := TRUE) FROM t",
4379            None,
4380        );
4381        check(DialectType::DuckDB, "UNION_VALUE(k1 := 1)", None);
4382
4383        // STRUCT_PACK(a := 'b')::json should at least parse without error
4384        // (The STRUCT_PACK -> Struct transformation is a separate feature)
4385        {
4386            let d = Dialect::get(DialectType::DuckDB);
4387            let ast = d
4388                .parse("STRUCT_PACK(a := 'b')::json")
4389                .expect("Failed to parse STRUCT_PACK(a := 'b')::json");
4390            assert!(!ast.is_empty(), "Empty AST for STRUCT_PACK(a := 'b')::json");
4391        }
4392
4393        // DuckDB prefix alias tests
4394        check(
4395            DialectType::DuckDB,
4396            "SELECT foo: 1",
4397            Some("SELECT 1 AS foo"),
4398        );
4399        check(
4400            DialectType::DuckDB,
4401            "SELECT foo: 1, bar: 2, baz: 3",
4402            Some("SELECT 1 AS foo, 2 AS bar, 3 AS baz"),
4403        );
4404    }
4405
4406    #[test]
4407    fn test_comment_roundtrip() {
4408        use crate::generator::Generator;
4409        use crate::parser::Parser;
4410
4411        fn check_roundtrip(sql: &str) -> Option<String> {
4412            let ast = match Parser::parse_sql(sql) {
4413                Ok(a) => a,
4414                Err(e) => return Some(format!("Parse error: {:?}", e)),
4415            };
4416            if ast.is_empty() {
4417                return Some("Empty AST".to_string());
4418            }
4419            let mut generator = Generator::default();
4420            let output = match generator.generate(&ast[0]) {
4421                Ok(o) => o,
4422                Err(e) => return Some(format!("Gen error: {:?}", e)),
4423            };
4424            if output == sql {
4425                None
4426            } else {
4427                Some(format!(
4428                    "Mismatch:\n  input:  {}\n  output: {}",
4429                    sql, output
4430                ))
4431            }
4432        }
4433
4434        let tests = vec![
4435            // Nested comments are sanitized: inner /* and */ are escaped
4436            // These no longer round-trip exactly (by design, matches Python sqlglot)
4437            // "SELECT c /* c1 /* c2 */ c3 */",        // becomes /* c1 / * c2 * / c3 */
4438            // "SELECT c /* c1 /* c2 /* c3 */ */ */",   // becomes /* c1 / * c2 / * c3 * / * / */
4439            // Simple alias with comments
4440            "SELECT c /* c1 */ AS alias /* c2 */",
4441            // Multiple columns with comments
4442            "SELECT a /* x */, b /* x */",
4443            // Multiple comments after column
4444            "SELECT a /* x */ /* y */ /* z */, b /* k */ /* m */",
4445            // FROM tables with comments
4446            "SELECT * FROM foo /* x */, bla /* x */",
4447            // Arithmetic with comments
4448            "SELECT 1 /* comment */ + 1",
4449            "SELECT 1 /* c1 */ + 2 /* c2 */",
4450            "SELECT 1 /* c1 */ + /* c2 */ 2 /* c3 */",
4451            // CAST with comments
4452            "SELECT CAST(x AS INT) /* comment */ FROM foo",
4453            // Function arguments with comments
4454            "SELECT FOO(x /* c */) /* FOO */, b /* b */",
4455            // Multi-part table names with comments
4456            "SELECT x FROM a.b.c /* x */, e.f.g /* x */",
4457            // INSERT with comments
4458            "INSERT INTO t1 (tc1 /* tc1 */, tc2 /* tc2 */) SELECT c1 /* sc1 */, c2 /* sc2 */ FROM t",
4459            // Leading comments on statements
4460            "/* c */ WITH x AS (SELECT 1) SELECT * FROM x",
4461            "/* comment1 */ INSERT INTO x /* comment2 */ VALUES (1, 2, 3)",
4462            "/* comment1 */ UPDATE tbl /* comment2 */ SET x = 2 WHERE x < 2",
4463            "/* comment1 */ DELETE FROM x /* comment2 */ WHERE y > 1",
4464            "/* comment */ CREATE TABLE foo AS SELECT 1",
4465            // Trailing comments on statements
4466            "INSERT INTO foo SELECT * FROM bar /* comment */",
4467            // Complex nested expressions with comments
4468            "SELECT FOO(x /* c1 */ + y /* c2 */ + BLA(5 /* c3 */)) FROM (VALUES (1 /* c4 */, \"test\" /* c5 */)) /* c6 */",
4469        ];
4470
4471        let mut failures = Vec::new();
4472        for sql in tests {
4473            if let Some(e) = check_roundtrip(sql) {
4474                failures.push(e);
4475            }
4476        }
4477
4478        if !failures.is_empty() {
4479            panic!("Comment roundtrip failures:\n{}", failures.join("\n\n"));
4480        }
4481    }
4482
4483    #[test]
4484    fn test_dollar_quoted_string_parsing() {
4485        use crate::dialects::{Dialect, DialectType};
4486
4487        // Test dollar string token parsing utility function
4488        let (tag, content) = super::parse_dollar_string_token("FOO\x00content here");
4489        assert_eq!(tag, Some("FOO".to_string()));
4490        assert_eq!(content, "content here");
4491
4492        let (tag, content) = super::parse_dollar_string_token("just content");
4493        assert_eq!(tag, None);
4494        assert_eq!(content, "just content");
4495
4496        // Test roundtrip for Databricks dialect with dollar-quoted function body
4497        fn check_databricks(sql: &str, expected: Option<&str>) {
4498            let d = Dialect::get(DialectType::Databricks);
4499            let ast = d
4500                .parse(sql)
4501                .unwrap_or_else(|e| panic!("Parse error for '{}': {}", sql, e));
4502            assert!(!ast.is_empty(), "Empty AST for: {}", sql);
4503            let transformed = d
4504                .transform(ast[0].clone())
4505                .unwrap_or_else(|e| panic!("Transform error for '{}': {}", sql, e));
4506            let output = d
4507                .generate(&transformed)
4508                .unwrap_or_else(|e| panic!("Generate error for '{}': {}", sql, e));
4509            let expected = expected.unwrap_or(sql);
4510            assert_eq!(output, expected, "Roundtrip failed for: {}", sql);
4511        }
4512
4513        // Test [42]: $$...$$ heredoc
4514        check_databricks(
4515            "CREATE FUNCTION add_one(x INT) RETURNS INT LANGUAGE PYTHON AS $$def add_one(x):\n  return x+1$$",
4516            None
4517        );
4518
4519        // Test [43]: $FOO$...$FOO$ tagged heredoc
4520        check_databricks(
4521            "CREATE FUNCTION add_one(x INT) RETURNS INT LANGUAGE PYTHON AS $FOO$def add_one(x):\n  return x+1$FOO$",
4522            None
4523        );
4524    }
4525
4526    #[test]
4527    fn test_numeric_underscore_stripping() {
4528        // Underscore stripping only happens when numbers_can_be_underscore_separated is true
4529        let mut config = TokenizerConfig::default();
4530        config.numbers_can_be_underscore_separated = true;
4531        let tokenizer = Tokenizer::new(config);
4532
4533        // Simple integer with underscores
4534        let tokens = tokenizer.tokenize("SELECT 1_2_3_4_5").unwrap();
4535        assert_eq!(tokens[1].token_type, TokenType::Number);
4536        assert_eq!(tokens[1].text, "12345");
4537
4538        // Thousands separator
4539        let tokens = tokenizer.tokenize("SELECT 20_000").unwrap();
4540        assert_eq!(tokens[1].token_type, TokenType::Number);
4541        assert_eq!(tokens[1].text, "20000");
4542
4543        // Scientific notation with underscores
4544        let tokens = tokenizer.tokenize("SELECT 1_2E+1_0").unwrap();
4545        assert_eq!(tokens[1].token_type, TokenType::Number);
4546        assert_eq!(tokens[1].text, "12E+10");
4547
4548        // Default tokenizer should NOT strip underscores
4549        let default_tokenizer = Tokenizer::default();
4550        let tokens = default_tokenizer.tokenize("SELECT 1_2_3_4_5").unwrap();
4551        assert_eq!(tokens[1].token_type, TokenType::Number);
4552        assert_eq!(tokens[1].text, "1_2_3_4_5");
4553    }
4554}