Skip to main content

polyglot_sql/
tokens.rs

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