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