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