Skip to main content

polyglot_sql/
tokens.rs

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