Skip to main content

polyglot_sql/
tokens.rs

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