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