Skip to main content

polyglot_sql/
tokens.rs

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