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 starts_numeric_exponent(&self) -> bool {
3019        if !matches!(self.peek(), 'e' | 'E') {
3020            return false;
3021        }
3022        let digits = self.current + 1 + usize::from(matches!(self.peek_next(), '+' | '-'));
3023        digits < self.size && self.char_at(digits).is_ascii_digit()
3024    }
3025
3026    fn scan_number(&mut self) -> Result<()> {
3027        // Check for 0x/0X hex number prefix (SQLite-style)
3028        if self.config.hex_number_strings && self.peek() == '0' && !self.is_at_end() {
3029            let next = if self.current + 1 < self.size {
3030                self.char_at(self.current + 1)
3031            } else {
3032                '\0'
3033            };
3034            if next == 'x' || next == 'X' {
3035                // Advance past '0' and 'x'/'X'
3036                self.advance();
3037                self.advance();
3038                // Collect hex digits (allow underscores as separators, e.g., 0xbad_cafe)
3039                let hex_start = self.current;
3040                if !self.advance_ascii_hex_digits() {
3041                    while !self.is_at_end()
3042                        && (self.peek().is_ascii_hexdigit() || self.peek() == '_')
3043                    {
3044                        if self.peek() == '_' && !self.peek_next().is_ascii_hexdigit() {
3045                            break;
3046                        }
3047                        self.advance();
3048                    }
3049                }
3050                let has_hex_digits = self.current > hex_start;
3051                let next_is_identifier_part = {
3052                    let next = self.peek();
3053                    next.is_alphanumeric() || matches!(next, '_' | '$' | '#' | '@')
3054                };
3055                let is_empty_hex_string = self.config.allow_empty_hex_string
3056                    && !self.config.hex_string_is_integer_type
3057                    && !next_is_identifier_part;
3058                if has_hex_digits || is_empty_hex_string {
3059                    // Check for hex float: 0xABC.DEFpEXP or 0xABCpEXP
3060                    let mut is_hex_float = false;
3061                    // Optional fractional part: .hexdigits
3062                    if has_hex_digits && !self.is_at_end() && self.peek() == '.' {
3063                        let after_dot = if self.current + 1 < self.size {
3064                            self.char_at(self.current + 1)
3065                        } else {
3066                            '\0'
3067                        };
3068                        if after_dot.is_ascii_hexdigit() {
3069                            is_hex_float = true;
3070                            self.advance(); // consume '.'
3071                            if !self.advance_ascii_hex_digits() {
3072                                while !self.is_at_end() && self.peek().is_ascii_hexdigit() {
3073                                    self.advance();
3074                                }
3075                            }
3076                        }
3077                    }
3078                    // Optional binary exponent: p/P [+/-] digits
3079                    if has_hex_digits
3080                        && !self.is_at_end()
3081                        && (self.peek() == 'p' || self.peek() == 'P')
3082                    {
3083                        is_hex_float = true;
3084                        self.advance(); // consume p/P
3085                        if !self.is_at_end() && (self.peek() == '+' || self.peek() == '-') {
3086                            self.advance();
3087                        }
3088                        if !self.advance_ascii_digits() {
3089                            while !self.is_at_end() && self.peek().is_ascii_digit() {
3090                                self.advance();
3091                            }
3092                        }
3093                    }
3094                    if is_hex_float {
3095                        // Hex float literal — emit as regular Number token with full text
3096                        let raw_text = self.text_from_range(self.start, self.current);
3097                        let full_text = if self.config.numbers_can_be_underscore_separated
3098                            && raw_text.contains('_')
3099                        {
3100                            raw_text.replace('_', "")
3101                        } else {
3102                            raw_text
3103                        };
3104                        self.add_token_with_text(TokenType::Number, full_text);
3105                    } else if self.config.hex_string_is_integer_type {
3106                        // BigQuery/ClickHouse: 0xA represents an integer in hex notation
3107                        let raw_value = self.text_from_range(hex_start, self.current);
3108                        let hex_value = if self.config.numbers_can_be_underscore_separated
3109                            && raw_value.contains('_')
3110                        {
3111                            raw_value.replace('_', "")
3112                        } else {
3113                            raw_value
3114                        };
3115                        self.add_token_with_text(TokenType::HexNumber, hex_value);
3116                    } else {
3117                        // SQLite/Teradata: 0xCC represents a binary/blob hex string
3118                        let raw_value = self.text_from_range(hex_start, self.current);
3119                        let hex_value = if self.config.numbers_can_be_underscore_separated
3120                            && raw_value.contains('_')
3121                        {
3122                            raw_value.replace('_', "")
3123                        } else {
3124                            raw_value
3125                        };
3126                        self.add_token_with_text(TokenType::HexString, hex_value);
3127                    }
3128                    return Ok(());
3129                }
3130                // No hex digits after 0x - fall through to normal number parsing
3131                // (reset current back to after '0')
3132                self.current = self.start + 1;
3133            }
3134        }
3135
3136        // Allow underscores as digit separators (e.g., 20_000, 1_000_000)
3137        if !self.advance_ascii_digits() {
3138            while !self.is_at_end() && (self.peek().is_ascii_digit() || self.peek() == '_') {
3139                // Don't allow underscore at the end (must be followed by digit)
3140                if self.peek() == '_' && (self.is_at_end() || !self.peek_next().is_ascii_digit()) {
3141                    break;
3142                }
3143                self.advance();
3144            }
3145        }
3146
3147        // Look for decimal part - allow trailing dot (e.g., "1.")
3148        // In PostgreSQL (and sqlglot), "1.x" parses as float "1." with alias "x"
3149        // So we always consume the dot as part of the number, even if followed by an identifier
3150        if self.peek() == '.' {
3151            let next = self.peek_next();
3152            // Only consume the dot if:
3153            // 1. Followed by a digit (normal decimal like 1.5)
3154            // 2. Followed by an identifier start (like 1.x -> becomes 1. with alias x)
3155            // 3. End of input or other non-dot character (trailing decimal like "1.")
3156            // Do NOT consume if it's a double dot (..) which is a range operator
3157            if next != '.' {
3158                self.advance(); // consume the .
3159                                // Only consume digits after the decimal point (not identifiers)
3160                if !self.advance_ascii_digits() {
3161                    while !self.is_at_end() && (self.peek().is_ascii_digit() || self.peek() == '_')
3162                    {
3163                        if self.peek() == '_' && !self.peek_next().is_ascii_digit() {
3164                            break;
3165                        }
3166                        self.advance();
3167                    }
3168                }
3169            }
3170        }
3171
3172        // Look for exponent
3173        if self.starts_numeric_exponent() {
3174            self.advance();
3175            if self.peek() == '+' || self.peek() == '-' {
3176                self.advance();
3177            }
3178            if !self.advance_ascii_digits() {
3179                while !self.is_at_end() && (self.peek().is_ascii_digit() || self.peek() == '_') {
3180                    if self.peek() == '_' && !self.peek_next().is_ascii_digit() {
3181                        break;
3182                    }
3183                    self.advance();
3184                }
3185            }
3186        }
3187
3188        let source_text = self
3189            .cursor
3190            .source_range(self.source, self.start, self.current);
3191        let raw_owned = source_text
3192            .is_none()
3193            .then(|| self.text_from_range(self.start, self.current));
3194        let raw_text = source_text.unwrap_or_else(|| {
3195            raw_owned
3196                .as_deref()
3197                .expect("non-ASCII numbers own their text")
3198        });
3199        // Strip underscore digit separators (e.g., 20_000 -> 20000, 1_2E+1_0 -> 12E+10)
3200        // Only for dialects that support this (ClickHouse, DuckDB)
3201        let normalized = (self.config.numbers_can_be_underscore_separated
3202            && raw_text.contains('_'))
3203        .then(|| raw_text.replace('_', ""));
3204        let text = normalized.as_deref().unwrap_or(raw_text);
3205
3206        // Check for numeric literal suffixes (e.g., 1L -> BIGINT, 1s -> SMALLINT in Hive/Spark)
3207        if !self.config.numeric_literals.is_empty() && !self.is_at_end() {
3208            let next_char: String = self.peek().to_ascii_uppercase().to_string();
3209            // Try 2-char suffix first (e.g., "BD"), then 1-char
3210            let suffix_match = if self.current + 1 < self.size {
3211                let two_char: String = [
3212                    self.char_at(self.current).to_ascii_uppercase(),
3213                    self.char_at(self.current + 1).to_ascii_uppercase(),
3214                ]
3215                .iter()
3216                .collect();
3217                if self.config.numeric_literals.contains_key(&two_char) {
3218                    // Make sure the 2-char suffix is not followed by more identifier chars
3219                    let after_suffix = if self.current + 2 < self.size {
3220                        self.char_at(self.current + 2)
3221                    } else {
3222                        ' '
3223                    };
3224                    if !after_suffix.is_alphanumeric() && after_suffix != '_' {
3225                        Some((two_char, 2))
3226                    } else {
3227                        None
3228                    }
3229                } else if self.config.numeric_literals.contains_key(&next_char) {
3230                    // 1-char suffix - make sure not followed by more identifier chars
3231                    let after_suffix = if self.current + 1 < self.size {
3232                        self.char_at(self.current + 1)
3233                    } else {
3234                        ' '
3235                    };
3236                    if !after_suffix.is_alphanumeric() && after_suffix != '_' {
3237                        Some((next_char, 1))
3238                    } else {
3239                        None
3240                    }
3241                } else {
3242                    None
3243                }
3244            } else if self.config.numeric_literals.contains_key(&next_char) {
3245                // At end of input, 1-char suffix
3246                Some((next_char, 1))
3247            } else {
3248                None
3249            };
3250
3251            if let Some((suffix, len)) = suffix_match {
3252                // Consume the suffix characters
3253                for _ in 0..len {
3254                    self.advance();
3255                }
3256                // Emit as a special number-with-suffix token
3257                // We'll encode as "number::TYPE" so the parser can split it
3258                let type_name = self
3259                    .config
3260                    .numeric_literals
3261                    .get(&suffix)
3262                    .expect("suffix verified by contains_key above")
3263                    .clone();
3264                let combined = format!("{}::{}", text, type_name);
3265                self.add_token_with_text(TokenType::Number, combined);
3266                return Ok(());
3267            }
3268        }
3269
3270        // Check for identifiers that start with a digit (e.g., 1a, 1_a, 1a_1a)
3271        // In Hive/Spark/MySQL/ClickHouse, these are valid unquoted identifiers
3272        if self.config.identifiers_can_start_with_digit && !self.is_at_end() {
3273            let next = self.peek();
3274            if next.is_alphabetic() || next == '_' {
3275                // Continue scanning as an identifier
3276                if !self.advance_ascii_identifier() {
3277                    while !self.is_at_end() {
3278                        let ch = self.peek();
3279                        if ch.is_alphanumeric() || ch == '_' {
3280                            self.advance();
3281                        } else {
3282                            break;
3283                        }
3284                    }
3285                }
3286                self.add_token(TokenType::Identifier);
3287                return Ok(());
3288            }
3289        }
3290
3291        if let Some(text) = normalized.or(raw_owned) {
3292            self.add_token_with_text(TokenType::Number, text);
3293        } else {
3294            self.add_token(TokenType::Number);
3295        }
3296        Ok(())
3297    }
3298
3299    /// Scan a number that starts with a dot (e.g., .25, .5, .123e10)
3300    fn scan_number_starting_with_dot(&mut self) -> Result<()> {
3301        // Consume the leading dot
3302        self.advance();
3303
3304        // Consume the fractional digits
3305        if !self.advance_ascii_digits() {
3306            while !self.is_at_end() && (self.peek().is_ascii_digit() || self.peek() == '_') {
3307                if self.peek() == '_' && !self.peek_next().is_ascii_digit() {
3308                    break;
3309                }
3310                self.advance();
3311            }
3312        }
3313
3314        // Look for exponent
3315        if self.starts_numeric_exponent() {
3316            self.advance();
3317            if self.peek() == '+' || self.peek() == '-' {
3318                self.advance();
3319            }
3320            if !self.advance_ascii_digits() {
3321                while !self.is_at_end() && (self.peek().is_ascii_digit() || self.peek() == '_') {
3322                    if self.peek() == '_' && !self.peek_next().is_ascii_digit() {
3323                        break;
3324                    }
3325                    self.advance();
3326                }
3327            }
3328        }
3329
3330        let source_text = self
3331            .cursor
3332            .source_range(self.source, self.start, self.current);
3333        let raw_owned = source_text
3334            .is_none()
3335            .then(|| self.text_from_range(self.start, self.current));
3336        let raw_text = source_text.unwrap_or_else(|| {
3337            raw_owned
3338                .as_deref()
3339                .expect("non-ASCII numbers own their text")
3340        });
3341        // Strip underscore digit separators (e.g., .1_5 -> .15)
3342        // Only for dialects that support this (ClickHouse, DuckDB)
3343        let normalized = (self.config.numbers_can_be_underscore_separated
3344            && raw_text.contains('_'))
3345        .then(|| raw_text.replace('_', ""));
3346        if let Some(text) = normalized.or(raw_owned) {
3347            self.add_token_with_text(TokenType::Number, text);
3348        } else {
3349            self.add_token(TokenType::Number);
3350        }
3351        Ok(())
3352    }
3353
3354    /// Look up a keyword using a stack buffer for ASCII uppercasing, avoiding heap allocation.
3355    /// Returns `TokenType::Var` for texts longer than 128 bytes or non-UTF-8 results.
3356    #[inline]
3357    fn lookup_keyword_ascii(keywords: &HashMap<String, TokenType>, text: &str) -> TokenType {
3358        if text.len() > 128 {
3359            return TokenType::Var;
3360        }
3361        let mut buf = [0u8; 128];
3362        for (i, b) in text.bytes().enumerate() {
3363            buf[i] = b.to_ascii_uppercase();
3364        }
3365        if let Ok(upper) = std::str::from_utf8(&buf[..text.len()]) {
3366            keywords.get(upper).copied().unwrap_or(TokenType::Var)
3367        } else {
3368            TokenType::Var
3369        }
3370    }
3371
3372    fn scan_identifier_or_keyword(&mut self) -> Result<()> {
3373        // Guard against unrecognized characters that could cause infinite loops
3374        let first_char = self.peek();
3375        if !first_char.is_alphanumeric() && first_char != '_' {
3376            // Unknown character - skip it and return an error
3377            let c = self.advance();
3378            return Err(Error::tokenize(
3379                format!("Unexpected character: '{}'", c),
3380                self.line,
3381                self.column,
3382                self.start,
3383                self.current,
3384            ));
3385        }
3386
3387        if !self.advance_ascii_identifier() {
3388            while !self.is_at_end() {
3389                let c = self.peek();
3390                // Allow alphanumeric, underscore, $, # and @ in identifiers
3391                // PostgreSQL allows $, TSQL allows # and @
3392                // But stop consuming # if followed by > or >> (PostgreSQL #> and #>> operators)
3393                if c == '#' {
3394                    let next_c = if self.current + 1 < self.size {
3395                        self.char_at(self.current + 1)
3396                    } else {
3397                        '\0'
3398                    };
3399                    if next_c == '>' || next_c == '-' {
3400                        break; // Don't consume # — it's part of #>, #>>, or #- operator
3401                    }
3402                    self.advance();
3403                } else if c.is_alphanumeric() || c == '_' || c == '$' || c == '@' {
3404                    self.advance();
3405                } else {
3406                    break;
3407                }
3408            }
3409        }
3410
3411        let source_text = self
3412            .cursor
3413            .source_range(self.source, self.start, self.current);
3414        let owned_text = source_text
3415            .is_none()
3416            .then(|| self.text_from_range(self.start, self.current));
3417        let text = source_text.unwrap_or_else(|| {
3418            owned_text
3419                .as_deref()
3420                .expect("non-ASCII identifiers own their text")
3421        });
3422
3423        // Special-case NOT= (Teradata and other dialects)
3424        if text.eq_ignore_ascii_case("NOT") && self.peek() == '=' {
3425            self.advance(); // consume '='
3426            self.add_token(TokenType::Neq);
3427            return Ok(());
3428        }
3429
3430        // Check for special string prefixes like N'...', X'...', B'...', U&'...', r'...', b'...'
3431        // Also handle double-quoted variants for dialects that support them (e.g., BigQuery)
3432        let next_char = self.peek();
3433        let is_single_quote = next_char == '\'';
3434        let is_double_quote = next_char == '"' && self.config.quotes.contains_key("\"");
3435        // For raw strings (r"..." or r'...'), we allow double quotes even if " is not in quotes config
3436        // because raw strings are a special case used in Spark/Databricks where " is for identifiers
3437        let is_double_quote_for_raw = next_char == '"';
3438
3439        // Handle raw strings first - they're special because they work with both ' and "
3440        // even in dialects where " is normally an identifier delimiter (like Databricks)
3441        if text.eq_ignore_ascii_case("R") && (is_single_quote || is_double_quote_for_raw) {
3442            // Raw string r'...' or r"..." or r'''...''' or r"""...""" (BigQuery style)
3443            // In raw strings, backslashes are treated literally (no escape processing)
3444            let quote_char = if is_single_quote { '\'' } else { '"' };
3445            self.advance(); // consume the first opening quote
3446
3447            // Check for triple-quoted raw string (r"""...""" or r'''...''')
3448            if self.peek() == quote_char && self.peek_next() == quote_char {
3449                // Triple-quoted raw string
3450                self.advance(); // consume second quote
3451                self.advance(); // consume third quote
3452                let string_value = self.scan_raw_triple_quoted_content(quote_char)?;
3453                self.add_token_with_text(TokenType::RawString, string_value);
3454            } else {
3455                let string_value = self.scan_raw_string_content(quote_char)?;
3456                self.add_token_with_text(TokenType::RawString, string_value);
3457            }
3458            return Ok(());
3459        }
3460
3461        if is_single_quote || is_double_quote {
3462            if text.eq_ignore_ascii_case("N") {
3463                // National string N'...'
3464                self.advance(); // consume the opening quote
3465                let string_value = if is_single_quote {
3466                    self.scan_string_content()?
3467                } else {
3468                    self.scan_double_quoted_string_content()?
3469                };
3470                self.add_token_with_text(TokenType::NationalString, string_value);
3471                return Ok(());
3472            } else if text.eq_ignore_ascii_case("E") {
3473                // PostgreSQL escape string E'...' or e'...'
3474                // Preserve the case by prefixing with "e:" or "E:"
3475                // Always use backslash escapes for escape strings (e.g., \' is an escaped quote)
3476                let lowercase = text == "e";
3477                let prefix = if lowercase { "e:" } else { "E:" };
3478                self.advance(); // consume the opening quote
3479                let string_value = self.scan_string_content_with_escapes(true)?;
3480                self.add_token_with_text(
3481                    TokenType::EscapeString,
3482                    format!("{}{}", prefix, string_value),
3483                );
3484                return Ok(());
3485            } else if text.eq_ignore_ascii_case("X") {
3486                // Hex string X'...'
3487                self.advance(); // consume the opening quote
3488                let string_value = if is_single_quote {
3489                    self.scan_string_content()?
3490                } else {
3491                    self.scan_double_quoted_string_content()?
3492                };
3493                self.add_token_with_text(TokenType::HexString, string_value);
3494                return Ok(());
3495            } else if text.eq_ignore_ascii_case("B") && is_double_quote {
3496                // Byte string b"..." (BigQuery style) - MUST check before single quote B'...'
3497                self.advance(); // consume the opening quote
3498                let string_value = self.scan_double_quoted_string_content()?;
3499                self.add_token_with_text(TokenType::ByteString, string_value);
3500                return Ok(());
3501            } else if text.eq_ignore_ascii_case("B") && is_single_quote {
3502                // For BigQuery: b'...' is a byte string (bytes data)
3503                // For standard SQL: B'...' is a bit string (binary digits)
3504                self.advance(); // consume the opening quote
3505                let string_value = self.scan_string_content()?;
3506                if self.config.b_prefix_is_byte_string {
3507                    self.add_token_with_text(TokenType::ByteString, string_value);
3508                } else {
3509                    self.add_token_with_text(TokenType::BitString, string_value);
3510                }
3511                return Ok(());
3512            }
3513        }
3514
3515        // Check for U&'...' Unicode string syntax (SQL standard)
3516        if text.eq_ignore_ascii_case("U")
3517            && self.peek() == '&'
3518            && self.current + 1 < self.size
3519            && self.char_at(self.current + 1) == '\''
3520        {
3521            self.advance(); // consume '&'
3522            self.advance(); // consume opening quote
3523            let string_value = self.scan_string_content()?;
3524            self.add_token_with_text(TokenType::UnicodeString, string_value);
3525            return Ok(());
3526        }
3527
3528        let token_type = Self::lookup_keyword_ascii(&self.config.keywords, &text);
3529
3530        if let Some(text) = owned_text {
3531            self.add_token_with_text(token_type, text);
3532        } else {
3533            self.add_token_from_source(token_type, self.start, self.current);
3534        }
3535        Ok(())
3536    }
3537
3538    /// Scan string content (everything between quotes)
3539    /// If `force_backslash_escapes` is true, backslash is always treated as an escape character
3540    /// (used for PostgreSQL E'...' escape strings)
3541    fn scan_string_content_with_escapes(
3542        &mut self,
3543        force_backslash_escapes: bool,
3544    ) -> Result<String> {
3545        let use_backslash_escapes =
3546            force_backslash_escapes || self.config.string_escapes.contains(&'\\');
3547        if let Some((start, end)) = self.try_scan_simple_quoted_content('\'', use_backslash_escapes)
3548        {
3549            return Ok(self.text_from_range(start, end));
3550        }
3551        let mut value = String::new();
3552
3553        while !self.is_at_end() {
3554            let c = self.peek();
3555            if c == '\'' {
3556                if self.peek_next() == '\'' {
3557                    // Escaped quote ''
3558                    value.push('\'');
3559                    self.advance();
3560                    self.advance();
3561                } else {
3562                    break;
3563                }
3564            } else if c == '\\' && use_backslash_escapes {
3565                // Preserve escape sequences literally (including \' for escape strings)
3566                value.push(self.advance());
3567                if !self.is_at_end() {
3568                    value.push(self.advance());
3569                }
3570            } else {
3571                value.push(self.advance());
3572            }
3573        }
3574
3575        if self.is_at_end() {
3576            return Err(Error::tokenize(
3577                "Unterminated string",
3578                self.line,
3579                self.column,
3580                self.start,
3581                self.current,
3582            ));
3583        }
3584
3585        self.advance(); // Closing quote
3586        Ok(value)
3587    }
3588
3589    /// Scan string content (everything between quotes)
3590    fn scan_string_content(&mut self) -> Result<String> {
3591        self.scan_string_content_with_escapes(false)
3592    }
3593
3594    /// Scan double-quoted string content (for dialects like BigQuery where " is a string delimiter)
3595    /// This is used for prefixed strings like b"..." or N"..."
3596    fn scan_double_quoted_string_content(&mut self) -> Result<String> {
3597        let use_backslash_escapes = self.config.string_escapes.contains(&'\\');
3598        if let Some((start, end)) = self.try_scan_simple_quoted_content('"', use_backslash_escapes)
3599        {
3600            return Ok(self.text_from_range(start, end));
3601        }
3602        let mut value = String::new();
3603
3604        while !self.is_at_end() {
3605            let c = self.peek();
3606            if c == '"' {
3607                if self.peek_next() == '"' {
3608                    // Escaped quote ""
3609                    value.push('"');
3610                    self.advance();
3611                    self.advance();
3612                } else {
3613                    break;
3614                }
3615            } else if c == '\\' && use_backslash_escapes {
3616                // Handle escape sequences
3617                self.advance(); // Consume backslash
3618                if !self.is_at_end() {
3619                    let escaped = self.advance();
3620                    match escaped {
3621                        'n' => value.push('\n'),
3622                        'r' => value.push('\r'),
3623                        't' => value.push('\t'),
3624                        '0' => value.push('\0'),
3625                        '\\' => value.push('\\'),
3626                        '"' => value.push('"'),
3627                        '\'' => value.push('\''),
3628                        'x' => {
3629                            // Hex escape \xNN - collect hex digits
3630                            let mut hex = String::new();
3631                            for _ in 0..2 {
3632                                if !self.is_at_end() && self.peek().is_ascii_hexdigit() {
3633                                    hex.push(self.advance());
3634                                }
3635                            }
3636                            if let Ok(byte) = u8::from_str_radix(&hex, 16) {
3637                                value.push(byte as char);
3638                            } else {
3639                                // Invalid hex escape, keep it literal
3640                                value.push('\\');
3641                                value.push('x');
3642                                value.push_str(&hex);
3643                            }
3644                        }
3645                        _ => {
3646                            // For unrecognized escapes, preserve backslash + char
3647                            value.push('\\');
3648                            value.push(escaped);
3649                        }
3650                    }
3651                }
3652            } else {
3653                value.push(self.advance());
3654            }
3655        }
3656
3657        if self.is_at_end() {
3658            return Err(Error::tokenize(
3659                "Unterminated double-quoted string",
3660                self.line,
3661                self.column,
3662                self.start,
3663                self.current,
3664            ));
3665        }
3666
3667        self.advance(); // Closing quote
3668        Ok(value)
3669    }
3670
3671    /// Scan raw string content (limited escape processing for quotes)
3672    /// Used for BigQuery r'...' and r"..." strings
3673    /// In raw strings, backslashes are literal EXCEPT that escape sequences for the
3674    /// quote character still work (e.g., \' in r'...' escapes the quote, '' also works)
3675    fn scan_raw_string_content(&mut self, quote_char: char) -> Result<String> {
3676        if let Some((start, end)) = self.try_scan_simple_quoted_content(
3677            quote_char,
3678            self.config.string_escapes_allowed_in_raw_strings,
3679        ) {
3680            return Ok(self.text_from_range(start, end));
3681        }
3682        let mut value = String::new();
3683
3684        while !self.is_at_end() {
3685            let c = self.peek();
3686            if c == quote_char {
3687                if self.peek_next() == quote_char {
3688                    // Escaped quote (doubled) - e.g., '' inside r'...'
3689                    value.push(quote_char);
3690                    self.advance();
3691                    self.advance();
3692                } else {
3693                    break;
3694                }
3695            } else if c == '\\'
3696                && self.peek_next() == quote_char
3697                && self.config.string_escapes_allowed_in_raw_strings
3698            {
3699                // The quote does not terminate the raw string, but both characters
3700                // remain literal content.
3701                value.push('\\');
3702                value.push(quote_char);
3703                self.advance(); // consume backslash
3704                self.advance(); // consume quote
3705            } else {
3706                // In raw strings, everything including backslashes is literal
3707                value.push(self.advance());
3708            }
3709        }
3710
3711        if self.is_at_end() {
3712            return Err(Error::tokenize(
3713                "Unterminated raw string",
3714                self.line,
3715                self.column,
3716                self.start,
3717                self.current,
3718            ));
3719        }
3720
3721        self.advance(); // Closing quote
3722        Ok(value)
3723    }
3724
3725    /// Scan raw triple-quoted string content (r"""...""" or r'''...''')
3726    /// Terminates when three consecutive quote_chars are found
3727    fn scan_raw_triple_quoted_content(&mut self, quote_char: char) -> Result<String> {
3728        let mut value = String::new();
3729
3730        while !self.is_at_end() {
3731            if self.peek() == quote_char {
3732                let mut quote_count = 0;
3733                while self.current + quote_count < self.size
3734                    && self.char_at(self.current + quote_count) == quote_char
3735                {
3736                    quote_count += 1;
3737                }
3738                if quote_count >= 3 {
3739                    // When more than three quotes occur, the leading quotes are content
3740                    // and the final three terminate the raw triple-quoted string.
3741                    for _ in 0..quote_count - 3 {
3742                        value.push(quote_char);
3743                    }
3744                    for _ in 0..quote_count {
3745                        self.advance();
3746                    }
3747                    return Ok(value);
3748                }
3749            }
3750            // In raw strings, everything including backslashes is literal
3751            let ch = self.advance();
3752            value.push(ch);
3753        }
3754
3755        Err(Error::tokenize(
3756            "Unterminated raw triple-quoted string",
3757            self.line,
3758            self.column,
3759            self.start,
3760            self.current,
3761        ))
3762    }
3763
3764    /// Scan TSQL identifiers that start with # (temp tables) or @ (variables)
3765    /// Examples: #temp, ##global_temp, @variable
3766    /// Scan an identifier that starts with `$` (ClickHouse).
3767    /// Examples: `$alias$name$`, `$x`
3768    fn scan_dollar_identifier(&mut self) -> Result<()> {
3769        // Consume the leading $
3770        self.advance();
3771
3772        // Consume alphanumeric, _, and $ continuation chars
3773        while !self.is_at_end() {
3774            let c = self.peek();
3775            if c.is_alphanumeric() || c == '_' || c == '$' {
3776                self.advance();
3777            } else {
3778                break;
3779            }
3780        }
3781
3782        self.add_token(TokenType::Var);
3783        Ok(())
3784    }
3785
3786    fn scan_tsql_identifier(&mut self) -> Result<()> {
3787        // Consume the leading # or @ (or ##)
3788        let first = self.advance();
3789
3790        // For ##, consume the second #
3791        if first == '#' && self.peek() == '#' {
3792            self.advance();
3793        }
3794
3795        // Now scan the rest of the identifier
3796        if !self.advance_ascii_identifier() {
3797            while !self.is_at_end() {
3798                let c = self.peek();
3799                if c.is_alphanumeric() || c == '_' || c == '$' || c == '#' || c == '@' {
3800                    self.advance();
3801                } else {
3802                    break;
3803                }
3804            }
3805        }
3806
3807        // These are always identifiers (variables or temp table names), never keywords
3808        self.add_token(TokenType::Var);
3809        Ok(())
3810    }
3811
3812    /// Check if the last tokens match INSERT ... FORMAT <name> (not VALUES).
3813    /// If so, consume everything until the next blank line (two consecutive newlines)
3814    /// or end of input as raw data.
3815    fn try_scan_insert_format_raw_data(&mut self) -> Option<String> {
3816        let len = self.tokens.len();
3817        if len < 3 {
3818            return None;
3819        }
3820
3821        // Last token should be the format name (Identifier or Var, not VALUES)
3822        let last = &self.tokens[len - 1];
3823        if last.text(self.source).eq_ignore_ascii_case("VALUES") {
3824            return None;
3825        }
3826        if !matches!(last.token_type(), TokenType::Var | TokenType::Identifier) {
3827            return None;
3828        }
3829
3830        // Second-to-last should be FORMAT
3831        let format_tok = &self.tokens[len - 2];
3832        if !format_tok.text(self.source).eq_ignore_ascii_case("FORMAT") {
3833            return None;
3834        }
3835
3836        // Check that there's an INSERT somewhere earlier in the tokens
3837        let has_insert = self.tokens[..len - 2]
3838            .iter()
3839            .rev()
3840            .take(20)
3841            .any(|t| t.token_type() == TokenType::Insert);
3842        if !has_insert {
3843            return None;
3844        }
3845
3846        // We're in INSERT ... FORMAT <name> context. Consume everything until:
3847        // - A blank line (two consecutive newlines, possibly with whitespace between)
3848        // - End of input
3849        let raw_start = self.current;
3850        while !self.is_at_end() {
3851            let c = self.peek();
3852            if c == '\n' {
3853                // Check for blank line: \n followed by optional \r and \n
3854                let saved = self.current;
3855                self.advance(); // consume first \n
3856                                // Skip \r if present
3857                while !self.is_at_end() && self.peek() == '\r' {
3858                    self.advance();
3859                }
3860                if self.is_at_end() || self.peek() == '\n' {
3861                    // Found blank line or end of input - stop here
3862                    // Don't consume the second \n so subsequent SQL can be tokenized
3863                    let raw = self.text_from_range(raw_start, saved);
3864                    return Some(raw.trim().to_string());
3865                }
3866                // Not a blank line, continue scanning
3867            } else {
3868                self.advance();
3869            }
3870        }
3871
3872        // Reached end of input
3873        let raw = self.text_from_range(raw_start, self.current);
3874        let trimmed = raw.trim().to_string();
3875        if trimmed.is_empty() {
3876            None
3877        } else {
3878            Some(trimmed)
3879        }
3880    }
3881
3882    fn add_token(&mut self, token_type: TokenType) {
3883        self.add_token_from_source(token_type, self.start, self.current);
3884    }
3885
3886    fn add_token_from_source(&mut self, token_type: TokenType, text_start: usize, text_end: usize) {
3887        let span = Span::new(self.start, self.current, self.line, self.column);
3888        if let Some(stats) = &mut self.guard_stats {
3889            stats.observe(token_type, span);
3890        }
3891        let mut token = if self
3892            .cursor
3893            .source_range(self.source, text_start, text_end)
3894            .is_some()
3895        {
3896            T::from_source(
3897                token_type,
3898                self.source,
3899                text_start,
3900                text_end,
3901                span,
3902                self.shared_source.as_ref(),
3903            )
3904        } else {
3905            T::from_owned(
3906                token_type,
3907                self.cursor
3908                    .text_from_range(self.source, text_start, text_end),
3909                span,
3910            )
3911        };
3912        token.comments_mut().append(&mut self.comments);
3913        self.tokens.push(token);
3914    }
3915
3916    fn add_token_with_text(&mut self, token_type: TokenType, text: String) {
3917        let span = Span::new(self.start, self.current, self.line, self.column);
3918        if let Some(stats) = &mut self.guard_stats {
3919            stats.observe(token_type, span);
3920        }
3921        let mut token = T::from_owned(token_type, text, span);
3922        token.comments_mut().append(&mut self.comments);
3923        self.tokens.push(token);
3924    }
3925}
3926
3927#[cfg(test)]
3928mod tests {
3929    use super::*;
3930
3931    #[test]
3932    fn ascii_fast_path_matches_character_buffer_path() {
3933        let tokenizer = Tokenizer::default();
3934        let inputs = [
3935            "SELECT a, b FROM t WHERE id IN (1, 2, 3)",
3936            "SELECT 'it''s', \"quoted\", $1 /* comment */ FROM schema.table",
3937            "INSERT INTO t VALUES (1, 'a'), (2, 'b'); UPDATE t SET value = 'c'",
3938            "SELECT $$body$$, $tag$content$tag$, 0xFF, 1.25e-2",
3939        ];
3940
3941        for sql in inputs {
3942            assert_eq!(
3943                tokenizer.tokenize(sql).unwrap(),
3944                tokenizer.tokenize_without_ascii_fast_path(sql).unwrap(),
3945                "tokenization differs for {sql}"
3946            );
3947        }
3948    }
3949
3950    #[test]
3951    fn parser_tokens_match_public_tokens() {
3952        let tokenizer = Tokenizer::default();
3953        let inputs = [
3954            "SELECT alpha, 123, 'plain' FROM schema.table WHERE id = 42",
3955            "SELECT 'it''s', $$body$$, $tag$content$tag$ /* comment */",
3956            "SELECT cafe, 'naive' FROM t\nWHERE value >= 1.25e-2",
3957            "SELECT cafe, 'caf\u{e9}', \u{3b4}elta FROM donn\u{e9}es",
3958        ];
3959
3960        for sql in inputs {
3961            let public = tokenizer.tokenize(sql).unwrap();
3962            let source: Arc<str> = Arc::from(sql);
3963            let (parser, stats) = tokenizer.tokenize_for_parser(&source).unwrap();
3964            let materialized = parser
3965                .iter()
3966                .map(|token| Token {
3967                    token_type: token.token_type,
3968                    text: token.text_owned(),
3969                    span: token.span,
3970                    comments: token.comments.clone(),
3971                    trailing_comments: token.trailing_comments.clone(),
3972                })
3973                .collect::<Vec<_>>();
3974
3975            assert_eq!(
3976                materialized, public,
3977                "parser tokenization differs for {sql}"
3978            );
3979            assert_eq!(stats.token_count, public.len());
3980        }
3981    }
3982
3983    #[test]
3984    fn parser_tokens_borrow_unchanged_ascii_text() {
3985        let tokenizer = Tokenizer::default();
3986        let source: Arc<str> = Arc::from("SELECT alpha, 123, 'plain'");
3987        let (tokens, _) = tokenizer.tokenize_for_parser(&source).unwrap();
3988
3989        assert!(tokens
3990            .iter()
3991            .all(|token| matches!(&token.text, ParserTokenText::Source { .. })));
3992    }
3993
3994    #[test]
3995    fn test_simple_select() {
3996        let tokenizer = Tokenizer::default();
3997        let tokens = tokenizer.tokenize("SELECT 1").unwrap();
3998
3999        assert_eq!(tokens.len(), 2);
4000        assert_eq!(tokens[0].token_type, TokenType::Select);
4001        assert_eq!(tokens[1].token_type, TokenType::Number);
4002        assert_eq!(tokens[1].text, "1");
4003    }
4004
4005    #[test]
4006    fn test_select_with_identifier() {
4007        let tokenizer = Tokenizer::default();
4008        let tokens = tokenizer.tokenize("SELECT a, b FROM t").unwrap();
4009
4010        assert_eq!(tokens.len(), 6);
4011        assert_eq!(tokens[0].token_type, TokenType::Select);
4012        assert_eq!(tokens[1].token_type, TokenType::Var);
4013        assert_eq!(tokens[1].text, "a");
4014        assert_eq!(tokens[2].token_type, TokenType::Comma);
4015        assert_eq!(tokens[3].token_type, TokenType::Var);
4016        assert_eq!(tokens[3].text, "b");
4017        assert_eq!(tokens[4].token_type, TokenType::From);
4018        assert_eq!(tokens[5].token_type, TokenType::Var);
4019        assert_eq!(tokens[5].text, "t");
4020    }
4021
4022    #[test]
4023    fn test_string_literal() {
4024        let tokenizer = Tokenizer::default();
4025        let tokens = tokenizer.tokenize("SELECT 'hello'").unwrap();
4026
4027        assert_eq!(tokens.len(), 2);
4028        assert_eq!(tokens[1].token_type, TokenType::String);
4029        assert_eq!(tokens[1].text, "hello");
4030    }
4031
4032    #[test]
4033    fn test_escaped_string() {
4034        let tokenizer = Tokenizer::default();
4035        let tokens = tokenizer.tokenize("SELECT 'it''s'").unwrap();
4036
4037        assert_eq!(tokens.len(), 2);
4038        assert_eq!(tokens[1].token_type, TokenType::String);
4039        assert_eq!(tokens[1].text, "it's");
4040    }
4041
4042    #[test]
4043    fn test_escape_follow_chars_gate_builtin_decoding() {
4044        let mut config = TokenizerConfig::default();
4045        config.string_escapes.push('\\');
4046        config.escape_follow_chars = vec!['n'];
4047        let tokenizer = Tokenizer::new(config);
4048        let tokens = tokenizer.tokenize(r"SELECT '\n\a\f\Z\x21'").unwrap();
4049
4050        assert_eq!(tokens[1].text, "\nafZx21");
4051    }
4052
4053    #[test]
4054    fn test_configured_numeric_escapes_require_complete_sequences() {
4055        let mut config = TokenizerConfig::default();
4056        config.string_escapes.push('\\');
4057        config.escape_follow_chars = vec!['0', '1', '2', '3', '4', '5', '6', '7', 'x', 'u'];
4058        let tokenizer = Tokenizer::new(config);
4059        let tokens = tokenizer
4060            .tokenize(r"SELECT '\041\x21\u26c4-\777\x2\u26c'")
4061            .unwrap();
4062
4063        assert_eq!(tokens[1].text, "!!\u{26c4}-777x2u26c");
4064    }
4065
4066    #[test]
4067    fn test_terminal_backslash_quote_recovery() {
4068        let mut config = TokenizerConfig::default();
4069        config.string_escapes.push('\\');
4070        config.recover_terminal_backslash_quote = true;
4071        let tokenizer = Tokenizer::new(config);
4072        let tokens = tokenizer
4073            .tokenize("SHOW FUNCTIONS LIKE 'a\\' OR 1=1")
4074            .unwrap();
4075
4076        assert_eq!(tokens.len(), 8);
4077        assert_eq!(tokens[3].token_type, TokenType::String);
4078        assert_eq!(tokens[3].text, "a\\");
4079        assert_eq!(tokens[4].token_type, TokenType::Or);
4080    }
4081
4082    #[test]
4083    fn test_comments() {
4084        let tokenizer = Tokenizer::default();
4085        let tokens = tokenizer.tokenize("SELECT -- comment\n1").unwrap();
4086
4087        assert_eq!(tokens.len(), 2);
4088        // Comments are attached to the PREVIOUS token as trailing_comments
4089        // This is better for round-trip fidelity (e.g., SELECT c /* comment */ FROM)
4090        assert_eq!(tokens[0].trailing_comments.len(), 1);
4091        assert_eq!(tokens[0].trailing_comments[0], " comment");
4092    }
4093
4094    #[test]
4095    fn test_comment_in_and_chain() {
4096        use crate::generator::Generator;
4097        use crate::parser::Parser;
4098
4099        // Line comments between AND clauses should appear after the AND operator
4100        let sql = "SELECT a FROM b WHERE foo\n-- c1\nAND bar\n-- c2\nAND bla";
4101        let ast = Parser::parse_sql(sql).unwrap();
4102        let mut gen = Generator::default();
4103        let output = gen.generate(&ast[0]).unwrap();
4104        assert_eq!(
4105            output,
4106            "SELECT a FROM b WHERE foo AND /* c1 */ bar AND /* c2 */ bla"
4107        );
4108    }
4109
4110    #[test]
4111    fn test_operators() {
4112        let tokenizer = Tokenizer::default();
4113        let tokens = tokenizer.tokenize("1 + 2 * 3").unwrap();
4114
4115        assert_eq!(tokens.len(), 5);
4116        assert_eq!(tokens[0].token_type, TokenType::Number);
4117        assert_eq!(tokens[1].token_type, TokenType::Plus);
4118        assert_eq!(tokens[2].token_type, TokenType::Number);
4119        assert_eq!(tokens[3].token_type, TokenType::Star);
4120        assert_eq!(tokens[4].token_type, TokenType::Number);
4121    }
4122
4123    #[test]
4124    fn test_comparison_operators() {
4125        let tokenizer = Tokenizer::default();
4126        let tokens = tokenizer.tokenize("a <= b >= c != d").unwrap();
4127
4128        assert_eq!(tokens[1].token_type, TokenType::Lte);
4129        assert_eq!(tokens[3].token_type, TokenType::Gte);
4130        assert_eq!(tokens[5].token_type, TokenType::Neq);
4131    }
4132
4133    #[test]
4134    fn test_national_string() {
4135        let tokenizer = Tokenizer::default();
4136        let tokens = tokenizer.tokenize("N'abc'").unwrap();
4137
4138        assert_eq!(
4139            tokens.len(),
4140            1,
4141            "Expected 1 token for N'abc', got {:?}",
4142            tokens
4143        );
4144        assert_eq!(tokens[0].token_type, TokenType::NationalString);
4145        assert_eq!(tokens[0].text, "abc");
4146    }
4147
4148    #[test]
4149    fn test_hex_string() {
4150        let tokenizer = Tokenizer::default();
4151        let tokens = tokenizer.tokenize("X'ABCD'").unwrap();
4152
4153        assert_eq!(
4154            tokens.len(),
4155            1,
4156            "Expected 1 token for X'ABCD', got {:?}",
4157            tokens
4158        );
4159        assert_eq!(tokens[0].token_type, TokenType::HexString);
4160        assert_eq!(tokens[0].text, "ABCD");
4161    }
4162
4163    #[test]
4164    fn test_bit_string() {
4165        let tokenizer = Tokenizer::default();
4166        let tokens = tokenizer.tokenize("B'01010'").unwrap();
4167
4168        assert_eq!(
4169            tokens.len(),
4170            1,
4171            "Expected 1 token for B'01010', got {:?}",
4172            tokens
4173        );
4174        assert_eq!(tokens[0].token_type, TokenType::BitString);
4175        assert_eq!(tokens[0].text, "01010");
4176    }
4177
4178    #[test]
4179    fn test_trailing_dot_number() {
4180        let tokenizer = Tokenizer::default();
4181
4182        // Test trailing dot
4183        let tokens = tokenizer.tokenize("SELECT 1.").unwrap();
4184        assert_eq!(
4185            tokens.len(),
4186            2,
4187            "Expected 2 tokens for 'SELECT 1.', got {:?}",
4188            tokens
4189        );
4190        assert_eq!(tokens[1].token_type, TokenType::Number);
4191        assert_eq!(tokens[1].text, "1.");
4192
4193        // Test normal decimal
4194        let tokens = tokenizer.tokenize("SELECT 1.5").unwrap();
4195        assert_eq!(tokens[1].text, "1.5");
4196
4197        // Test number followed by dot and identifier
4198        // In PostgreSQL (and sqlglot), "1.x" parses as float "1." with alias "x"
4199        let tokens = tokenizer.tokenize("SELECT 1.a").unwrap();
4200        assert_eq!(
4201            tokens.len(),
4202            3,
4203            "Expected 3 tokens for 'SELECT 1.a', got {:?}",
4204            tokens
4205        );
4206        assert_eq!(tokens[1].token_type, TokenType::Number);
4207        assert_eq!(tokens[1].text, "1.");
4208        assert_eq!(tokens[2].token_type, TokenType::Var);
4209
4210        // Test two dots (range operator) - dot is NOT consumed when followed by another dot
4211        let tokens = tokenizer.tokenize("SELECT 1..2").unwrap();
4212        assert_eq!(tokens[1].token_type, TokenType::Number);
4213        assert_eq!(tokens[1].text, "1");
4214        assert_eq!(tokens[2].token_type, TokenType::Dot);
4215        assert_eq!(tokens[3].token_type, TokenType::Dot);
4216        assert_eq!(tokens[4].token_type, TokenType::Number);
4217        assert_eq!(tokens[4].text, "2");
4218    }
4219
4220    #[test]
4221    fn test_leading_dot_number() {
4222        let tokenizer = Tokenizer::default();
4223
4224        // Test leading dot number (e.g., .25 for 0.25)
4225        let tokens = tokenizer.tokenize(".25").unwrap();
4226        assert_eq!(
4227            tokens.len(),
4228            1,
4229            "Expected 1 token for '.25', got {:?}",
4230            tokens
4231        );
4232        assert_eq!(tokens[0].token_type, TokenType::Number);
4233        assert_eq!(tokens[0].text, ".25");
4234
4235        // Test leading dot in context (Oracle SAMPLE clause)
4236        let tokens = tokenizer.tokenize("SAMPLE (.25)").unwrap();
4237        assert_eq!(
4238            tokens.len(),
4239            4,
4240            "Expected 4 tokens for 'SAMPLE (.25)', got {:?}",
4241            tokens
4242        );
4243        assert_eq!(tokens[0].token_type, TokenType::Sample);
4244        assert_eq!(tokens[1].token_type, TokenType::LParen);
4245        assert_eq!(tokens[2].token_type, TokenType::Number);
4246        assert_eq!(tokens[2].text, ".25");
4247        assert_eq!(tokens[3].token_type, TokenType::RParen);
4248
4249        // Test leading dot with exponent
4250        let tokens = tokenizer.tokenize(".5e10").unwrap();
4251        assert_eq!(
4252            tokens.len(),
4253            1,
4254            "Expected 1 token for '.5e10', got {:?}",
4255            tokens
4256        );
4257        assert_eq!(tokens[0].token_type, TokenType::Number);
4258        assert_eq!(tokens[0].text, ".5e10");
4259
4260        // Test that plain dot is still a Dot token
4261        let tokens = tokenizer.tokenize("a.b").unwrap();
4262        assert_eq!(
4263            tokens.len(),
4264            3,
4265            "Expected 3 tokens for 'a.b', got {:?}",
4266            tokens
4267        );
4268        assert_eq!(tokens[1].token_type, TokenType::Dot);
4269    }
4270
4271    #[test]
4272    fn test_unrecognized_character() {
4273        let tokenizer = Tokenizer::default();
4274
4275        // Unicode curly quotes are now handled as string delimiters
4276        let result = tokenizer.tokenize("SELECT \u{2018}hello\u{2019}");
4277        assert!(
4278            result.is_ok(),
4279            "Curly quotes should be tokenized as strings"
4280        );
4281
4282        // Unicode bullet character should still error
4283        let result = tokenizer.tokenize("SELECT • FROM t");
4284        assert!(result.is_err());
4285    }
4286
4287    #[test]
4288    fn test_colon_eq_tokenization() {
4289        let tokenizer = Tokenizer::default();
4290
4291        // := should be a single ColonEq token
4292        let tokens = tokenizer.tokenize("a := 1").unwrap();
4293        assert_eq!(tokens.len(), 3);
4294        assert_eq!(tokens[0].token_type, TokenType::Var);
4295        assert_eq!(tokens[1].token_type, TokenType::ColonEq);
4296        assert_eq!(tokens[2].token_type, TokenType::Number);
4297
4298        // : followed by non-= should still be Colon
4299        let tokens = tokenizer.tokenize("a:b").unwrap();
4300        assert!(tokens.iter().any(|t| t.token_type == TokenType::Colon));
4301        assert!(!tokens.iter().any(|t| t.token_type == TokenType::ColonEq));
4302
4303        // :: should still be DColon
4304        let tokens = tokenizer.tokenize("a::INT").unwrap();
4305        assert!(tokens.iter().any(|t| t.token_type == TokenType::DColon));
4306    }
4307
4308    #[test]
4309    fn test_colon_eq_parsing() {
4310        use crate::generator::Generator;
4311        use crate::parser::Parser;
4312
4313        // MySQL @var := value in SELECT
4314        let ast = Parser::parse_sql("SELECT @var1 := 1, @var2")
4315            .expect("Failed to parse MySQL @var := expr");
4316        let output = Generator::sql(&ast[0]).expect("Failed to generate");
4317        assert_eq!(output, "SELECT @var1 := 1, @var2");
4318
4319        // MySQL @var := @var in SELECT
4320        let ast = Parser::parse_sql("SELECT @var1, @var2 := @var1")
4321            .expect("Failed to parse MySQL @var2 := @var1");
4322        let output = Generator::sql(&ast[0]).expect("Failed to generate");
4323        assert_eq!(output, "SELECT @var1, @var2 := @var1");
4324
4325        // MySQL @var := COUNT(*)
4326        let ast = Parser::parse_sql("SELECT @var1 := COUNT(*) FROM t1")
4327            .expect("Failed to parse MySQL @var := COUNT(*)");
4328        let output = Generator::sql(&ast[0]).expect("Failed to generate");
4329        assert_eq!(output, "SELECT @var1 := COUNT(*) FROM t1");
4330
4331        // MySQL SET @var := 1 (should normalize to = in output)
4332        let ast = Parser::parse_sql("SET @var1 := 1").expect("Failed to parse SET @var1 := 1");
4333        let output = Generator::sql(&ast[0]).expect("Failed to generate");
4334        assert_eq!(output, "SET @var1 = 1");
4335
4336        // Function named args with :=
4337        let ast =
4338            Parser::parse_sql("UNION_VALUE(k1 := 1)").expect("Failed to parse named arg with :=");
4339        let output = Generator::sql(&ast[0]).expect("Failed to generate");
4340        assert_eq!(output, "UNION_VALUE(k1 := 1)");
4341
4342        // UNNEST with recursive := TRUE
4343        let ast = Parser::parse_sql("SELECT UNNEST(col, recursive := TRUE) FROM t")
4344            .expect("Failed to parse UNNEST with :=");
4345        let output = Generator::sql(&ast[0]).expect("Failed to generate");
4346        assert_eq!(output, "SELECT UNNEST(col, recursive := TRUE) FROM t");
4347
4348        // DuckDB prefix alias: foo: 1 means 1 AS foo
4349        let ast =
4350            Parser::parse_sql("SELECT foo: 1").expect("Failed to parse DuckDB prefix alias foo: 1");
4351        let output = Generator::sql(&ast[0]).expect("Failed to generate");
4352        assert_eq!(output, "SELECT 1 AS foo");
4353
4354        // DuckDB prefix alias with multiple columns
4355        let ast = Parser::parse_sql("SELECT foo: 1, bar: 2, baz: 3")
4356            .expect("Failed to parse DuckDB multiple prefix aliases");
4357        let output = Generator::sql(&ast[0]).expect("Failed to generate");
4358        assert_eq!(output, "SELECT 1 AS foo, 2 AS bar, 3 AS baz");
4359    }
4360
4361    #[test]
4362    fn test_colon_eq_dialect_roundtrip() {
4363        use crate::dialects::{Dialect, DialectType};
4364
4365        fn check(dialect: DialectType, sql: &str, expected: Option<&str>) {
4366            let d = Dialect::get(dialect);
4367            let ast = d
4368                .parse(sql)
4369                .unwrap_or_else(|e| panic!("Parse error for '{}': {}", sql, e));
4370            assert!(!ast.is_empty(), "Empty AST for: {}", sql);
4371            let transformed = d
4372                .transform(ast[0].clone())
4373                .unwrap_or_else(|e| panic!("Transform error for '{}': {}", sql, e));
4374            let output = d
4375                .generate(&transformed)
4376                .unwrap_or_else(|e| panic!("Generate error for '{}': {}", sql, e));
4377            let expected = expected.unwrap_or(sql);
4378            assert_eq!(output, expected, "Roundtrip failed for: {}", sql);
4379        }
4380
4381        // MySQL := tests
4382        check(DialectType::MySQL, "SELECT @var1 := 1, @var2", None);
4383        check(DialectType::MySQL, "SELECT @var1, @var2 := @var1", None);
4384        check(DialectType::MySQL, "SELECT @var1 := COUNT(*) FROM t1", None);
4385        check(DialectType::MySQL, "SET @var1 := 1", Some("SET @var1 = 1"));
4386
4387        // DuckDB := tests
4388        check(
4389            DialectType::DuckDB,
4390            "SELECT UNNEST(col, recursive := TRUE) FROM t",
4391            None,
4392        );
4393        check(DialectType::DuckDB, "UNION_VALUE(k1 := 1)", None);
4394
4395        // STRUCT_PACK(a := 'b')::json should at least parse without error
4396        // (The STRUCT_PACK -> Struct transformation is a separate feature)
4397        {
4398            let d = Dialect::get(DialectType::DuckDB);
4399            let ast = d
4400                .parse("STRUCT_PACK(a := 'b')::json")
4401                .expect("Failed to parse STRUCT_PACK(a := 'b')::json");
4402            assert!(!ast.is_empty(), "Empty AST for STRUCT_PACK(a := 'b')::json");
4403        }
4404
4405        // DuckDB prefix alias tests
4406        check(
4407            DialectType::DuckDB,
4408            "SELECT foo: 1",
4409            Some("SELECT 1 AS foo"),
4410        );
4411        check(
4412            DialectType::DuckDB,
4413            "SELECT foo: 1, bar: 2, baz: 3",
4414            Some("SELECT 1 AS foo, 2 AS bar, 3 AS baz"),
4415        );
4416    }
4417
4418    #[test]
4419    fn test_comment_roundtrip() {
4420        use crate::generator::Generator;
4421        use crate::parser::Parser;
4422
4423        fn check_roundtrip(sql: &str) -> Option<String> {
4424            let ast = match Parser::parse_sql(sql) {
4425                Ok(a) => a,
4426                Err(e) => return Some(format!("Parse error: {:?}", e)),
4427            };
4428            if ast.is_empty() {
4429                return Some("Empty AST".to_string());
4430            }
4431            let mut generator = Generator::default();
4432            let output = match generator.generate(&ast[0]) {
4433                Ok(o) => o,
4434                Err(e) => return Some(format!("Gen error: {:?}", e)),
4435            };
4436            if output == sql {
4437                None
4438            } else {
4439                Some(format!(
4440                    "Mismatch:\n  input:  {}\n  output: {}",
4441                    sql, output
4442                ))
4443            }
4444        }
4445
4446        let tests = vec![
4447            // Nested comments are sanitized: inner /* and */ are escaped
4448            // These no longer round-trip exactly (by design, matches Python sqlglot)
4449            // "SELECT c /* c1 /* c2 */ c3 */",        // becomes /* c1 / * c2 * / c3 */
4450            // "SELECT c /* c1 /* c2 /* c3 */ */ */",   // becomes /* c1 / * c2 / * c3 * / * / */
4451            // Simple alias with comments
4452            "SELECT c /* c1 */ AS alias /* c2 */",
4453            // Multiple columns with comments
4454            "SELECT a /* x */, b /* x */",
4455            // Multiple comments after column
4456            "SELECT a /* x */ /* y */ /* z */, b /* k */ /* m */",
4457            // FROM tables with comments
4458            "SELECT * FROM foo /* x */, bla /* x */",
4459            // Arithmetic with comments
4460            "SELECT 1 /* comment */ + 1",
4461            "SELECT 1 /* c1 */ + 2 /* c2 */",
4462            "SELECT 1 /* c1 */ + /* c2 */ 2 /* c3 */",
4463            // CAST with comments
4464            "SELECT CAST(x AS INT) /* comment */ FROM foo",
4465            // Function arguments with comments
4466            "SELECT FOO(x /* c */) /* FOO */, b /* b */",
4467            // Multi-part table names with comments
4468            "SELECT x FROM a.b.c /* x */, e.f.g /* x */",
4469            // INSERT with comments
4470            "INSERT INTO t1 (tc1 /* tc1 */, tc2 /* tc2 */) SELECT c1 /* sc1 */, c2 /* sc2 */ FROM t",
4471            // Leading comments on statements
4472            "/* c */ WITH x AS (SELECT 1) SELECT * FROM x",
4473            "/* comment1 */ INSERT INTO x /* comment2 */ VALUES (1, 2, 3)",
4474            "/* comment1 */ UPDATE tbl /* comment2 */ SET x = 2 WHERE x < 2",
4475            "/* comment1 */ DELETE FROM x /* comment2 */ WHERE y > 1",
4476            "/* comment */ CREATE TABLE foo AS SELECT 1",
4477            // Trailing comments on statements
4478            "INSERT INTO foo SELECT * FROM bar /* comment */",
4479            // Complex nested expressions with comments
4480            "SELECT FOO(x /* c1 */ + y /* c2 */ + BLA(5 /* c3 */)) FROM (VALUES (1 /* c4 */, \"test\" /* c5 */)) /* c6 */",
4481        ];
4482
4483        let mut failures = Vec::new();
4484        for sql in tests {
4485            if let Some(e) = check_roundtrip(sql) {
4486                failures.push(e);
4487            }
4488        }
4489
4490        if !failures.is_empty() {
4491            panic!("Comment roundtrip failures:\n{}", failures.join("\n\n"));
4492        }
4493    }
4494
4495    #[test]
4496    fn test_dollar_quoted_string_parsing() {
4497        use crate::dialects::{Dialect, DialectType};
4498
4499        // Test dollar string token parsing utility function
4500        let (tag, content) = super::parse_dollar_string_token("FOO\x00content here");
4501        assert_eq!(tag, Some("FOO".to_string()));
4502        assert_eq!(content, "content here");
4503
4504        let (tag, content) = super::parse_dollar_string_token("just content");
4505        assert_eq!(tag, None);
4506        assert_eq!(content, "just content");
4507
4508        // Test roundtrip for Databricks dialect with dollar-quoted function body
4509        fn check_databricks(sql: &str, expected: Option<&str>) {
4510            let d = Dialect::get(DialectType::Databricks);
4511            let ast = d
4512                .parse(sql)
4513                .unwrap_or_else(|e| panic!("Parse error for '{}': {}", sql, e));
4514            assert!(!ast.is_empty(), "Empty AST for: {}", sql);
4515            let transformed = d
4516                .transform(ast[0].clone())
4517                .unwrap_or_else(|e| panic!("Transform error for '{}': {}", sql, e));
4518            let output = d
4519                .generate(&transformed)
4520                .unwrap_or_else(|e| panic!("Generate error for '{}': {}", sql, e));
4521            let expected = expected.unwrap_or(sql);
4522            assert_eq!(output, expected, "Roundtrip failed for: {}", sql);
4523        }
4524
4525        // Test [42]: $$...$$ heredoc
4526        check_databricks(
4527            "CREATE FUNCTION add_one(x INT) RETURNS INT LANGUAGE PYTHON AS $$def add_one(x):\n  return x+1$$",
4528            None
4529        );
4530
4531        // Test [43]: $FOO$...$FOO$ tagged heredoc
4532        check_databricks(
4533            "CREATE FUNCTION add_one(x INT) RETURNS INT LANGUAGE PYTHON AS $FOO$def add_one(x):\n  return x+1$FOO$",
4534            None
4535        );
4536    }
4537
4538    #[test]
4539    fn test_numeric_underscore_stripping() {
4540        // Underscore stripping only happens when numbers_can_be_underscore_separated is true
4541        let mut config = TokenizerConfig::default();
4542        config.numbers_can_be_underscore_separated = true;
4543        let tokenizer = Tokenizer::new(config);
4544
4545        // Simple integer with underscores
4546        let tokens = tokenizer.tokenize("SELECT 1_2_3_4_5").unwrap();
4547        assert_eq!(tokens[1].token_type, TokenType::Number);
4548        assert_eq!(tokens[1].text, "12345");
4549
4550        // Thousands separator
4551        let tokens = tokenizer.tokenize("SELECT 20_000").unwrap();
4552        assert_eq!(tokens[1].token_type, TokenType::Number);
4553        assert_eq!(tokens[1].text, "20000");
4554
4555        // Scientific notation with underscores
4556        let tokens = tokenizer.tokenize("SELECT 1_2E+1_0").unwrap();
4557        assert_eq!(tokens[1].token_type, TokenType::Number);
4558        assert_eq!(tokens[1].text, "12E+10");
4559
4560        // Default tokenizer should NOT strip underscores
4561        let default_tokenizer = Tokenizer::default();
4562        let tokens = default_tokenizer.tokenize("SELECT 1_2_3_4_5").unwrap();
4563        assert_eq!(tokens[1].token_type, TokenType::Number);
4564        assert_eq!(tokens[1].text, "1_2_3_4_5");
4565    }
4566}