Skip to main content

fsqlite_parser/
lexer.rs

1// bd-2tu6: §10.1 SQL Lexer
2//
3// Converts SQL text into a stream of tokens. Uses memchr for accelerated
4// string scanning. Tracks line/column for error reporting.
5
6use fsqlite_ast::Span;
7use fsqlite_types::limits::MAX_VARIABLE_NUMBER;
8use hashbrown::HashSet;
9use memchr::memchr;
10use std::sync::Arc;
11use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
12use std::time::Instant;
13use tracing::Level;
14
15use crate::token::{Token, TokenKind};
16
17/// Histogram buckets for `fsqlite_tokenize_duration_seconds`.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19pub struct TokenizeDurationSecondsHistogram {
20    /// Duration <= 100 µs.
21    pub le_100us: u64,
22    /// Duration <= 250 µs.
23    pub le_250us: u64,
24    /// Duration <= 500 µs.
25    pub le_500us: u64,
26    /// Duration <= 1 ms.
27    pub le_1ms: u64,
28    /// Duration <= 5 ms.
29    pub le_5ms: u64,
30    /// Duration > 5 ms.
31    pub gt_5ms: u64,
32}
33
34/// Point-in-time tokenize metric snapshot.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
36pub struct TokenizeMetricsSnapshot {
37    /// Monotonic token counter across all tokenize calls.
38    pub fsqlite_tokenize_tokens_total: u64,
39    /// Histogram buckets for tokenize runtime.
40    pub fsqlite_tokenize_duration_seconds: TokenizeDurationSecondsHistogram,
41    /// Total tokenize observations recorded in histogram.
42    pub fsqlite_tokenize_duration_seconds_count: u64,
43    /// Sum of tokenize durations in microseconds.
44    pub fsqlite_tokenize_duration_seconds_sum_micros: u64,
45}
46
47static FSQLITE_TOKENIZE_TOKENS_TOTAL: AtomicU64 = AtomicU64::new(0);
48static FSQLITE_TOKENIZE_DURATION_SECONDS_LE_100US: AtomicU64 = AtomicU64::new(0);
49static FSQLITE_TOKENIZE_DURATION_SECONDS_LE_250US: AtomicU64 = AtomicU64::new(0);
50static FSQLITE_TOKENIZE_DURATION_SECONDS_LE_500US: AtomicU64 = AtomicU64::new(0);
51static FSQLITE_TOKENIZE_DURATION_SECONDS_LE_1MS: AtomicU64 = AtomicU64::new(0);
52static FSQLITE_TOKENIZE_DURATION_SECONDS_LE_5MS: AtomicU64 = AtomicU64::new(0);
53static FSQLITE_TOKENIZE_DURATION_SECONDS_GT_5MS: AtomicU64 = AtomicU64::new(0);
54static FSQLITE_TOKENIZE_DURATION_SECONDS_COUNT: AtomicU64 = AtomicU64::new(0);
55static FSQLITE_TOKENIZE_DURATION_SECONDS_SUM_MICROS: AtomicU64 = AtomicU64::new(0);
56static FSQLITE_TOKENIZE_METRICS_ENABLED: AtomicBool = AtomicBool::new(false);
57
58fn saturating_u64_from_usize(value: usize) -> u64 {
59    u64::try_from(value).unwrap_or(u64::MAX)
60}
61
62fn saturating_u64_from_u128(value: u128) -> u64 {
63    u64::try_from(value).unwrap_or(u64::MAX)
64}
65
66fn record_tokenize_metrics(token_count: usize, elapsed_micros: u64) {
67    FSQLITE_TOKENIZE_TOKENS_TOTAL
68        .fetch_add(saturating_u64_from_usize(token_count), Ordering::Relaxed);
69    FSQLITE_TOKENIZE_DURATION_SECONDS_COUNT.fetch_add(1, Ordering::Relaxed);
70    FSQLITE_TOKENIZE_DURATION_SECONDS_SUM_MICROS.fetch_add(elapsed_micros, Ordering::Relaxed);
71
72    let bucket = match elapsed_micros {
73        0..=100 => &FSQLITE_TOKENIZE_DURATION_SECONDS_LE_100US,
74        101..=250 => &FSQLITE_TOKENIZE_DURATION_SECONDS_LE_250US,
75        251..=500 => &FSQLITE_TOKENIZE_DURATION_SECONDS_LE_500US,
76        501..=1_000 => &FSQLITE_TOKENIZE_DURATION_SECONDS_LE_1MS,
77        1_001..=5_000 => &FSQLITE_TOKENIZE_DURATION_SECONDS_LE_5MS,
78        _ => &FSQLITE_TOKENIZE_DURATION_SECONDS_GT_5MS,
79    };
80    bucket.fetch_add(1, Ordering::Relaxed);
81}
82
83/// Point-in-time snapshot of tokenize metrics.
84#[must_use]
85pub fn tokenize_metrics_snapshot() -> TokenizeMetricsSnapshot {
86    TokenizeMetricsSnapshot {
87        fsqlite_tokenize_tokens_total: FSQLITE_TOKENIZE_TOKENS_TOTAL.load(Ordering::Relaxed),
88        fsqlite_tokenize_duration_seconds: TokenizeDurationSecondsHistogram {
89            le_100us: FSQLITE_TOKENIZE_DURATION_SECONDS_LE_100US.load(Ordering::Relaxed),
90            le_250us: FSQLITE_TOKENIZE_DURATION_SECONDS_LE_250US.load(Ordering::Relaxed),
91            le_500us: FSQLITE_TOKENIZE_DURATION_SECONDS_LE_500US.load(Ordering::Relaxed),
92            le_1ms: FSQLITE_TOKENIZE_DURATION_SECONDS_LE_1MS.load(Ordering::Relaxed),
93            le_5ms: FSQLITE_TOKENIZE_DURATION_SECONDS_LE_5MS.load(Ordering::Relaxed),
94            gt_5ms: FSQLITE_TOKENIZE_DURATION_SECONDS_GT_5MS.load(Ordering::Relaxed),
95        },
96        fsqlite_tokenize_duration_seconds_count: FSQLITE_TOKENIZE_DURATION_SECONDS_COUNT
97            .load(Ordering::Relaxed),
98        fsqlite_tokenize_duration_seconds_sum_micros: FSQLITE_TOKENIZE_DURATION_SECONDS_SUM_MICROS
99            .load(Ordering::Relaxed),
100    }
101}
102
103/// Enable or disable tokenize metrics collection on the hot path.
104pub fn set_tokenize_metrics_enabled(enabled: bool) {
105    FSQLITE_TOKENIZE_METRICS_ENABLED.store(enabled, Ordering::Relaxed);
106}
107
108/// Return whether tokenize metrics collection is enabled.
109#[must_use]
110pub fn tokenize_metrics_enabled() -> bool {
111    FSQLITE_TOKENIZE_METRICS_ENABLED.load(Ordering::Relaxed)
112}
113
114/// Reset tokenize metrics (used by tests/diagnostics).
115pub fn reset_tokenize_metrics() {
116    FSQLITE_TOKENIZE_TOKENS_TOTAL.store(0, Ordering::Relaxed);
117    FSQLITE_TOKENIZE_DURATION_SECONDS_LE_100US.store(0, Ordering::Relaxed);
118    FSQLITE_TOKENIZE_DURATION_SECONDS_LE_250US.store(0, Ordering::Relaxed);
119    FSQLITE_TOKENIZE_DURATION_SECONDS_LE_500US.store(0, Ordering::Relaxed);
120    FSQLITE_TOKENIZE_DURATION_SECONDS_LE_1MS.store(0, Ordering::Relaxed);
121    FSQLITE_TOKENIZE_DURATION_SECONDS_LE_5MS.store(0, Ordering::Relaxed);
122    FSQLITE_TOKENIZE_DURATION_SECONDS_GT_5MS.store(0, Ordering::Relaxed);
123    FSQLITE_TOKENIZE_DURATION_SECONDS_COUNT.store(0, Ordering::Relaxed);
124    FSQLITE_TOKENIZE_DURATION_SECONDS_SUM_MICROS.store(0, Ordering::Relaxed);
125}
126
127/// Bound retained identifier interners so scratch reuse does not grow without limit.
128const MAX_RETAINED_IDENTIFIER_INTERNER_ENTRIES: usize = 256;
129const MAX_RETAINED_IDENTIFIER_INTERNER_BYTES: usize = 16 * 1024;
130
131/// SQL lexer that produces a stream of tokens from source text.
132#[derive(Debug, Default)]
133pub(crate) struct IdentifierInterner {
134    values: HashSet<Arc<str>>,
135}
136
137impl IdentifierInterner {
138    fn intern(&mut self, value: &str) -> Arc<str> {
139        if let Some(existing) = self.values.get(value) {
140            return Arc::clone(existing);
141        }
142
143        let interned: Arc<str> = Arc::from(value);
144        let inserted = Arc::clone(&interned);
145        self.values.insert(interned);
146        inserted
147    }
148
149    pub(crate) fn reset(&mut self) {
150        self.values = HashSet::new();
151    }
152
153    pub(crate) fn retained_bytes(&self) -> usize {
154        let interned_value_bytes = self
155            .values
156            .iter()
157            .fold(0usize, |sum, value| sum.saturating_add(value.len()));
158        self.values
159            .capacity()
160            .saturating_mul(std::mem::size_of::<Arc<str>>())
161            .saturating_add(interned_value_bytes)
162    }
163
164    pub(crate) fn prepare_for_next_parse(&mut self) {
165        if self.values.len() > MAX_RETAINED_IDENTIFIER_INTERNER_ENTRIES
166            || self.retained_bytes() > MAX_RETAINED_IDENTIFIER_INTERNER_BYTES
167        {
168            self.reset();
169        }
170    }
171
172    #[cfg(test)]
173    pub(crate) fn is_empty(&self) -> bool {
174        self.values.is_empty()
175    }
176
177    #[cfg(test)]
178    pub(crate) fn len(&self) -> usize {
179        self.values.len()
180    }
181}
182
183/// SQL lexer that produces a stream of tokens from source text.
184pub struct Lexer<'a> {
185    /// The source bytes (UTF-8).
186    src: &'a [u8],
187    /// Current byte offset into src.
188    pos: usize,
189    /// Current line number (1-based).
190    line: u32,
191    /// Current column number (1-based).
192    col: u32,
193    /// Whether TRACE character-level logging is enabled.
194    trace_chars: bool,
195    /// Per-parse identifier interner reused by scratch callers.
196    interner: IdentifierInterner,
197}
198
199impl<'a> Lexer<'a> {
200    fn log_token(token: &Token) {
201        tracing::debug!(
202            target: "fsqlite.parse",
203            token = ?token.kind,
204            start = token.span.start,
205            end = token.span.end,
206            line = token.line,
207            col = token.col,
208            "tokenized token"
209        );
210    }
211
212    /// Create a new lexer for the given SQL source text.
213    #[must_use]
214    pub fn new(source: &'a str) -> Self {
215        Self {
216            src: source.as_bytes(),
217            pos: 0,
218            line: 1,
219            col: 1,
220            trace_chars: tracing::enabled!(target: "fsqlite.parse", Level::TRACE),
221            interner: IdentifierInterner::default(),
222        }
223    }
224
225    /// Tokenize the entire input into a Vec of tokens.
226    #[must_use]
227    pub fn tokenize(source: &'a str) -> Vec<Token> {
228        let mut tokens = Vec::new();
229        Self::tokenize_into(source, &mut tokens);
230        tokens
231    }
232
233    fn new_with_interner(source: &'a str, interner: IdentifierInterner) -> Self {
234        Self {
235            src: source.as_bytes(),
236            pos: 0,
237            line: 1,
238            col: 1,
239            trace_chars: tracing::enabled!(target: "fsqlite.parse", Level::TRACE),
240            interner,
241        }
242    }
243
244    /// Tokenize the entire input into a caller-owned buffer.
245    ///
246    /// This preserves the buffer's existing heap allocation across repeated
247    /// parses so statement-level callers can treat token storage as lookaside
248    /// scratch instead of rebuilding a fresh `Vec<Token>` on every miss.
249    pub fn tokenize_into(source: &'a str, tokens: &mut Vec<Token>) {
250        let mut interner = IdentifierInterner::default();
251        Self::tokenize_into_with_interner(source, tokens, &mut interner);
252    }
253
254    pub(crate) fn tokenize_into_with_interner(
255        source: &'a str,
256        tokens: &mut Vec<Token>,
257        interner: &mut IdentifierInterner,
258    ) {
259        let input_bytes = source.len();
260        let collect_tokenize_metrics = tokenize_metrics_enabled();
261        let trace_tokenize = tracing::enabled!(target: "fsqlite.parse", Level::TRACE);
262        let span = trace_tokenize.then(|| {
263            tracing::span!(
264                target: "fsqlite.parse",
265                Level::TRACE,
266                "tokenize",
267                token_count = tracing::field::Empty,
268                input_bytes,
269                elapsed_us = tracing::field::Empty,
270            )
271        });
272        let _guard = span.as_ref().map(|span| span.enter());
273        let started = (collect_tokenize_metrics || trace_tokenize).then(Instant::now);
274
275        let mut lexer = Self::new_with_interner(source, std::mem::take(interner));
276        let target_capacity = input_bytes / 4 + 1;
277        tokens.clear();
278        if target_capacity > tokens.capacity() {
279            tokens.reserve(target_capacity - tokens.capacity());
280        }
281        loop {
282            let tok = lexer.next_token();
283            let is_eof = tok.kind == TokenKind::Eof;
284            tokens.push(tok);
285            if is_eof {
286                break;
287            }
288        }
289
290        *interner = lexer.interner;
291
292        if let Some(started) = started {
293            let elapsed_us = saturating_u64_from_u128(started.elapsed().as_micros());
294            if let Some(span) = span.as_ref() {
295                span.record("token_count", saturating_u64_from_usize(tokens.len()));
296                span.record("elapsed_us", elapsed_us);
297            }
298            if collect_tokenize_metrics {
299                record_tokenize_metrics(tokens.len(), elapsed_us);
300            }
301        }
302    }
303
304    /// Expose tokenize metrics as a snapshot.
305    #[must_use]
306    pub fn metrics_snapshot() -> TokenizeMetricsSnapshot {
307        tokenize_metrics_snapshot()
308    }
309
310    /// Reset tokenize metrics.
311    pub fn reset_metrics() {
312        reset_tokenize_metrics();
313    }
314
315    /// Produce the next token.
316    pub fn next_token(&mut self) -> Token {
317        self.skip_whitespace_and_comments();
318
319        if self.pos >= self.src.len() {
320            let token = self.make_token(TokenKind::Eof, self.pos, self.pos);
321            Self::log_token(&token);
322            return token;
323        }
324
325        let start = self.pos;
326        let start_line = self.line;
327        let start_col = self.col;
328        let ch = self.src[self.pos];
329
330        let kind = match ch {
331            // String literal (single-quoted)
332            b'\'' => self.lex_string(),
333
334            // Double-quoted identifier
335            b'"' => self.lex_double_quoted_id(),
336
337            // Backtick-quoted identifier
338            b'`' => self.lex_backtick_id(),
339
340            // Bracket-quoted identifier
341            b'[' => self.lex_bracket_id(),
342
343            // Blob literal or hex
344            b'X' | b'x' if self.peek_at(1) == Some(b'\'') => self.lex_blob(),
345
346            // Numbers
347            b'0'..=b'9' => self.lex_number(),
348            b'.' if self.peek_at(1).is_some_and(|c| c.is_ascii_digit()) => self.lex_number(),
349
350            // Identifiers and keywords
351            b'a'..=b'z' | b'A'..=b'Z' | b'_' | 0x80..=0xFF => self.lex_identifier(),
352
353            // Bind parameters
354            b'?' => self.lex_question(),
355            b':' => self.lex_colon_param(),
356            b'@' => self.lex_at_param(),
357            b'$' => self.lex_dollar_param(),
358
359            // Operators and punctuation
360            b'+' => {
361                self.advance();
362                TokenKind::Plus
363            }
364            b'*' => {
365                self.advance();
366                TokenKind::Star
367            }
368            b'/' => {
369                self.advance();
370                TokenKind::Slash
371            }
372            b'%' => {
373                self.advance();
374                TokenKind::Percent
375            }
376            b'&' => {
377                self.advance();
378                TokenKind::Ampersand
379            }
380            b'~' => {
381                self.advance();
382                TokenKind::Tilde
383            }
384            b',' => {
385                self.advance();
386                TokenKind::Comma
387            }
388            b';' => {
389                self.advance();
390                TokenKind::Semicolon
391            }
392            b'(' => {
393                self.advance();
394                TokenKind::LeftParen
395            }
396            b')' => {
397                self.advance();
398                TokenKind::RightParen
399            }
400            b'.' => {
401                self.advance();
402                TokenKind::Dot
403            }
404
405            // Multi-character operators
406            b'-' => self.lex_minus_or_arrow(),
407            b'<' => self.lex_lt(),
408            b'>' => self.lex_gt(),
409            b'=' => self.lex_eq(),
410            b'!' => self.lex_bang(),
411            b'|' => self.lex_pipe(),
412
413            _ => {
414                self.advance();
415                let s = String::from_utf8_lossy(&self.src[start..self.pos]).into_owned();
416                TokenKind::Error(format!("unexpected character: {s}"))
417            }
418        };
419
420        let token = Token {
421            kind,
422            #[allow(clippy::cast_possible_truncation)]
423            span: Span::new(start as u32, self.pos as u32),
424            line: start_line,
425            col: start_col,
426        };
427
428        Self::log_token(&token);
429        token
430    }
431
432    // -----------------------------------------------------------------------
433    // Helpers
434    // -----------------------------------------------------------------------
435
436    #[allow(clippy::cast_possible_truncation)]
437    fn advance_by(&mut self, n: usize) {
438        if n == 0 {
439            return;
440        }
441        let end = self.pos + n;
442        let slice = &self.src[self.pos..end];
443        #[allow(clippy::naive_bytecount)]
444        let newlines = slice.iter().filter(|&&b| b == b'\n').count();
445        if newlines > 0 {
446            self.line += newlines as u32;
447            let last_nl = slice.iter().rposition(|&b| b == b'\n').unwrap_or(0);
448            self.col = (n - last_nl) as u32;
449        } else {
450            self.col += n as u32;
451        }
452        self.pos = end;
453    }
454
455    fn advance(&mut self) -> u8 {
456        let pos = self.pos;
457        let line = self.line;
458        let col = self.col;
459        let ch = self.src[self.pos];
460        self.pos += 1;
461        if ch == b'\n' {
462            self.line += 1;
463            self.col = 1;
464        } else {
465            self.col += 1;
466        }
467        if self.trace_chars {
468            tracing::trace!(
469                target: "fsqlite.parse",
470                byte = ch,
471                pos,
472                line,
473                col,
474                "tokenize char"
475            );
476        }
477        ch
478    }
479
480    fn peek(&self) -> Option<u8> {
481        self.src.get(self.pos).copied()
482    }
483
484    fn peek_at(&self, offset: usize) -> Option<u8> {
485        self.src.get(self.pos + offset).copied()
486    }
487
488    #[allow(clippy::cast_possible_truncation)]
489    fn make_token(&self, kind: TokenKind, start: usize, end: usize) -> Token {
490        Token {
491            kind,
492            span: Span::new(start as u32, end as u32),
493            line: self.line,
494            col: self.col,
495        }
496    }
497
498    /// Skip whitespace, line comments (`--`), and block comments (`/* */`).
499    fn skip_whitespace_and_comments(&mut self) {
500        loop {
501            // Skip whitespace
502            let mut ws_len = 0;
503            while self.pos + ws_len < self.src.len()
504                && self.src[self.pos + ws_len].is_ascii_whitespace()
505            {
506                ws_len += 1;
507            }
508            if ws_len > 0 {
509                self.advance_by(ws_len);
510            }
511
512            if self.pos >= self.src.len() {
513                break;
514            }
515
516            // Line comment: `-- ...`
517            if self.src[self.pos] == b'-' && self.peek_at(1) == Some(b'-') {
518                self.advance(); // skip -
519                self.advance(); // skip -
520                while self.pos < self.src.len() && self.src[self.pos] != b'\n' {
521                    self.advance();
522                }
523                continue;
524            }
525
526            // Block comment: `/* ... */` (SQLite does NOT support nesting)
527            if self.src[self.pos] == b'/' && self.peek_at(1) == Some(b'*') {
528                self.advance(); // skip /
529                self.advance(); // skip *
530                let closed = loop {
531                    if self.pos >= self.src.len() {
532                        break false;
533                    }
534                    if self.src[self.pos] == b'*' && self.peek_at(1) == Some(b'/') {
535                        self.advance();
536                        self.advance();
537                        break true;
538                    }
539                    self.advance();
540                };
541                if !closed {
542                    // Unclosed block comment consumes to EOF
543                    self.pos = self.src.len();
544                }
545                continue;
546            }
547
548            break;
549        }
550    }
551
552    // -----------------------------------------------------------------------
553    // Literal tokenizers
554    // -----------------------------------------------------------------------
555
556    fn lex_string(&mut self) -> TokenKind {
557        let start = self.pos;
558        self.advance(); // skip opening quote
559
560        let mut value = String::new();
561        loop {
562            // Use memchr to find the next single quote quickly
563            let remaining = &self.src[self.pos..];
564            if let Some(offset) = memchr(b'\'', remaining) {
565                // Append bytes up to the quote
566                value.push_str(&String::from_utf8_lossy(
567                    &self.src[self.pos..self.pos + offset],
568                ));
569                // Advance past the accumulated bytes and the quote
570                self.advance_by(offset);
571                self.advance(); // the quote itself
572
573                // Check for escaped quote ('')
574                if self.peek() == Some(b'\'') {
575                    value.push('\'');
576                    self.advance();
577                } else {
578                    return TokenKind::String(value);
579                }
580            } else {
581                // Unterminated string
582                self.pos = self.src.len();
583                return TokenKind::Error(format!(
584                    "unterminated string literal starting at byte {}",
585                    start
586                ));
587            }
588        }
589    }
590
591    /// Lex a double-quoted identifier. Sets the EP_DblQuoted flag.
592    fn lex_double_quoted_id(&mut self) -> TokenKind {
593        let start = self.pos;
594        self.advance(); // skip opening "
595
596        let mut value = String::new();
597        loop {
598            let remaining = &self.src[self.pos..];
599            if let Some(offset) = memchr(b'"', remaining) {
600                value.push_str(&String::from_utf8_lossy(
601                    &self.src[self.pos..self.pos + offset],
602                ));
603                self.advance_by(offset);
604                self.advance(); // the quote
605
606                // Doubled-quote escape: "" -> "
607                if self.peek() == Some(b'"') {
608                    value.push('"');
609                    self.advance();
610                } else {
611                    return TokenKind::QuotedId(self.interner.intern(&value), true);
612                }
613            } else {
614                self.pos = self.src.len();
615                return TokenKind::Error(format!(
616                    "unterminated double-quoted identifier at byte {}",
617                    start
618                ));
619            }
620        }
621    }
622
623    /// Lex a backtick-quoted identifier.
624    fn lex_backtick_id(&mut self) -> TokenKind {
625        let start = self.pos;
626        self.advance(); // skip `
627
628        let mut value = String::new();
629        loop {
630            let remaining = &self.src[self.pos..];
631            if let Some(offset) = memchr(b'`', remaining) {
632                value.push_str(&String::from_utf8_lossy(
633                    &self.src[self.pos..self.pos + offset],
634                ));
635                self.advance_by(offset);
636                self.advance(); // the backtick
637
638                if self.peek() == Some(b'`') {
639                    value.push('`');
640                    self.advance();
641                } else {
642                    return TokenKind::QuotedId(self.interner.intern(&value), false);
643                }
644            } else {
645                self.pos = self.src.len();
646                return TokenKind::Error(format!(
647                    "unterminated backtick identifier at byte {}",
648                    start
649                ));
650            }
651        }
652    }
653
654    /// Lex a bracket-quoted identifier `[name]`.
655    fn lex_bracket_id(&mut self) -> TokenKind {
656        let start = self.pos;
657        self.advance(); // skip [
658
659        let mut value = String::new();
660        let remaining = &self.src[self.pos..];
661        if let Some(offset) = memchr(b']', remaining) {
662            value.push_str(&String::from_utf8_lossy(
663                &self.src[self.pos..self.pos + offset],
664            ));
665            self.advance_by(offset);
666            self.advance(); // skip ]
667            TokenKind::QuotedId(self.interner.intern(&value), false)
668        } else {
669            self.pos = self.src.len();
670            TokenKind::Error(format!("unterminated bracket identifier at byte {}", start))
671        }
672    }
673
674    /// Lex a blob literal `X'...'` / `x'...'`.
675    fn lex_blob(&mut self) -> TokenKind {
676        let start = self.pos;
677        self.advance(); // skip X/x
678        self.advance(); // skip '
679
680        let hex_start = self.pos;
681        let remaining = &self.src[self.pos..];
682        if let Some(offset) = memchr(b'\'', remaining) {
683            let hex_bytes = &self.src[hex_start..hex_start + offset];
684            self.advance_by(offset);
685            self.advance(); // skip closing '
686
687            // Validate hex content
688            if hex_bytes.len() % 2 != 0 {
689                return TokenKind::Error(format!(
690                    "blob literal has odd number of hex digits at byte {}",
691                    start
692                ));
693            }
694
695            // Work directly on raw bytes to avoid panics from
696            // string-slicing multi-byte UTF-8 sequences.
697            let mut bytes = Vec::with_capacity(hex_bytes.len() / 2);
698            for pair in hex_bytes.chunks_exact(2) {
699                let hi = hex_digit(pair[0]);
700                let lo = hex_digit(pair[1]);
701                match (hi, lo) {
702                    (Some(h), Some(l)) => bytes.push((h << 4) | l),
703                    _ => {
704                        return TokenKind::Error(format!(
705                            "invalid hex in blob literal at byte {start}"
706                        ));
707                    }
708                }
709            }
710            TokenKind::Blob(bytes)
711        } else {
712            self.pos = self.src.len();
713            TokenKind::Error(format!("unterminated blob literal at byte {}", start))
714        }
715    }
716
717    /// Lex a number: integer, hex integer, or float.
718    fn lex_number(&mut self) -> TokenKind {
719        let start = self.pos;
720
721        // Check for hex prefix
722        if self.src[self.pos] == b'0' && self.peek_at(1).is_some_and(|c| c == b'x' || c == b'X') {
723            self.advance(); // 0
724            self.advance(); // x
725            let hex_start = self.pos;
726            // Consume hex digits, allowing a single `_` digit separator that is
727            // surrounded on both sides by a hex digit (SQLite 3.46+ digit
728            // separators). `0x_1F`, `0x1__F`, and a trailing `0x1F_` are
729            // rejected because the underscore is not between two hex digits.
730            while self.pos < self.src.len() {
731                let c = self.src[self.pos];
732                let is_separator = c == b'_'
733                    && self.pos > hex_start
734                    && self.src[self.pos - 1].is_ascii_hexdigit()
735                    && self.peek_at(1).is_some_and(|n| n.is_ascii_hexdigit());
736                if c.is_ascii_hexdigit() || is_separator {
737                    self.advance();
738                } else {
739                    break;
740                }
741            }
742            if self.pos == hex_start {
743                return TokenKind::Error("empty hex literal".to_owned());
744            }
745            // A hex literal cannot be immediately followed by an alphanumeric or
746            // `_` (e.g. `0x1F_` or `0xFFg`) — that is an unrecognized token, the
747            // same rule the decimal path enforces below.
748            if self
749                .peek()
750                .is_some_and(|n| n.is_ascii_alphanumeric() || n == b'_')
751            {
752                let err_start = start;
753                while self.pos < self.src.len()
754                    && (self.src[self.pos].is_ascii_alphanumeric() || self.src[self.pos] == b'_')
755                {
756                    self.advance();
757                }
758                let err_text = String::from_utf8_lossy(&self.src[err_start..self.pos]);
759                return TokenKind::Error(format!("unrecognized token: \"{err_text}\""));
760            }
761            let hex_str = String::from_utf8_lossy(&self.src[hex_start..self.pos]).replace('_', "");
762            // Strip leading zeros then check significant digit count,
763            // matching C SQLite's sqlite3DecOrHexToI64 which rejects
764            // hex literals with >16 significant digits.
765            let significant = hex_str.trim_start_matches('0');
766            if significant.len() > 16 {
767                return TokenKind::Error(format!("hex literal out of range at byte {start}"));
768            }
769            let parse_str = if significant.is_empty() {
770                "0"
771            } else {
772                significant
773            };
774            // Parse as u64 and bitwise-cast to i64 — matching C SQLite's
775            // sqlite3DecOrHexToI64 which uses memcpy(pOut, &u, 8).
776            return match u64::from_str_radix(parse_str, 16) {
777                Ok(v) => {
778                    #[allow(clippy::cast_possible_wrap)]
779                    let i = v as i64;
780                    TokenKind::Integer(i)
781                }
782                Err(_) => TokenKind::Error(format!("hex literal out of range at byte {start}")),
783            };
784        }
785
786        // Decimal integer or float
787        let mut is_float = false;
788
789        // Integer part (may be empty for `.5` style). Allow `_` digit
790        // separators that are surrounded on both sides by a decimal digit.
791        self.consume_decimal_digit_run();
792
793        // Helper to check if the current position (+ offset) starts a valid exponent.
794        let is_valid_exponent = |lexer: &Self, mut offset: usize| -> bool {
795            if let Some(c) = lexer.peek_at(offset) {
796                if c == b'e' || c == b'E' {
797                    offset += 1;
798                    if let Some(s) = lexer.peek_at(offset) {
799                        if s == b'+' || s == b'-' {
800                            offset += 1;
801                        }
802                    }
803                    if let Some(d) = lexer.peek_at(offset) {
804                        return d.is_ascii_digit();
805                    }
806                }
807            }
808            false
809        };
810
811        // Fractional part
812        if self.pos < self.src.len()
813            && self.src[self.pos] == b'.'
814            && (self.peek_at(1).is_some_and(|c| c.is_ascii_digit()) || is_valid_exponent(self, 1))
815        {
816            is_float = true;
817            self.advance(); // skip dot
818            self.consume_decimal_digit_run();
819        } else if self.pos < self.src.len()
820            && self.src[self.pos] == b'.'
821            && start < self.pos // we had digits before the dot
822            && !self.peek_at(1).is_some_and(|c| c.is_ascii_alphanumeric() || c == b'_')
823        {
824            // e.g. `123.` with nothing meaningful after -- still a float
825            is_float = true;
826            self.advance(); // skip dot
827        }
828
829        // Handle case where input starts with '.'
830        if self.src[start] == b'.' {
831            is_float = true;
832        }
833
834        // Exponent
835        if is_valid_exponent(self, 0) {
836            is_float = true;
837            self.advance(); // skip e/E
838            if self.pos < self.src.len()
839                && (self.src[self.pos] == b'+' || self.src[self.pos] == b'-')
840            {
841                self.advance();
842            }
843            self.consume_decimal_digit_run();
844        }
845
846        // SQLite strictness: a number cannot be immediately followed by an alphabetical character or underscore.
847        // Doing so produces an "unrecognized token" error.
848        if let Some(c) = self.peek() {
849            if c.is_ascii_alphabetic()
850                || c == b'_'
851                || (c == b'.'
852                    && self
853                        .peek_at(1)
854                        .is_some_and(|n| n.is_ascii_alphabetic() || n == b'_'))
855            {
856                let err_start = start;
857                while self.pos < self.src.len() {
858                    let ch = self.src[self.pos];
859                    if ch.is_ascii_alphanumeric() || ch == b'_' || ch == b'.' {
860                        self.advance();
861                    } else {
862                        break;
863                    }
864                }
865                let err_text = String::from_utf8_lossy(&self.src[err_start..self.pos]);
866                return TokenKind::Error(format!("unrecognized token: \"{err_text}\""));
867            }
868        }
869
870        let text_raw = String::from_utf8_lossy(&self.src[start..self.pos]);
871        // Strip `_` digit separators before numeric parsing; they were only
872        // consumed when validly placed between two digits above.
873        let text: std::borrow::Cow<'_, str> = if text_raw.as_ref().contains('_') {
874            std::borrow::Cow::Owned(text_raw.replace('_', ""))
875        } else {
876            text_raw
877        };
878        if is_float {
879            // Overflowing literals (e.g. 9e999) become ±Infinity, exactly like
880            // C SQLite's text-to-real conversion; do NOT clamp to f64::MAX.
881            match text.parse::<f64>() {
882                Ok(v) => TokenKind::Float(v),
883                Err(_) => {
884                    // Rust's f64 parser rejects `.e4` but SQLite accepts it as 0.0.
885                    let mut text_fixed = text.clone().into_owned();
886                    if text_fixed.starts_with(".e") || text_fixed.starts_with(".E") {
887                        text_fixed.insert(0, '0');
888                    }
889                    match text_fixed.parse::<f64>() {
890                        Ok(v) => TokenKind::Float(v),
891                        Err(_) => TokenKind::Error(format!("invalid float: {text}")),
892                    }
893                }
894            }
895        } else {
896            match text.parse::<i64>() {
897                Ok(v) => TokenKind::Integer(v),
898                Err(_) => {
899                    // SQLite promotes oversized integers to REAL. We emit a special
900                    // token to allow the parser to fold `-9223372036854775808` correctly.
901                    TokenKind::OversizedInt(text.into_owned())
902                }
903            }
904        }
905    }
906
907    /// Consume a run of decimal digits, allowing single `_` digit separators
908    /// (SQLite 3.46+) that are surrounded on both sides by a decimal digit. An
909    /// underscore that is leading, trailing, doubled, or adjacent to a non-digit
910    /// (`.`, `e`, sign) terminates the run, leaving it to be flagged as an
911    /// unrecognized token by the trailing-character check in `lex_number`.
912    fn consume_decimal_digit_run(&mut self) {
913        while self.pos < self.src.len() {
914            let c = self.src[self.pos];
915            let is_separator = c == b'_'
916                && self.pos > 0
917                && self.src[self.pos - 1].is_ascii_digit()
918                && self.peek_at(1).is_some_and(|n| n.is_ascii_digit());
919            if c.is_ascii_digit() || is_separator {
920                self.advance();
921            } else {
922                break;
923            }
924        }
925    }
926
927    /// Lex an identifier or keyword.
928    fn lex_identifier(&mut self) -> TokenKind {
929        let start = self.pos;
930        self.advance(); // first character already validated
931
932        while self.pos < self.src.len() {
933            let ch = self.src[self.pos];
934            if ch.is_ascii_alphanumeric() || ch == b'_' || ch >= 0x80 {
935                self.advance();
936            } else {
937                break;
938            }
939        }
940
941        let ident_bytes = &self.src[start..self.pos];
942
943        // Check for keyword
944        if let Some(kw) = TokenKind::lookup_keyword_bytes(ident_bytes) {
945            kw
946        } else {
947            let text = String::from_utf8_lossy(ident_bytes);
948            TokenKind::Id(self.interner.intern(&text))
949        }
950    }
951
952    /// Lex `?` or `?NNN`.
953    fn lex_question(&mut self) -> TokenKind {
954        self.advance(); // skip ?
955        if self.pos < self.src.len() && self.src[self.pos].is_ascii_digit() {
956            let num_start = self.pos;
957            while self.pos < self.src.len() && self.src[self.pos].is_ascii_digit() {
958                self.advance();
959            }
960            let text = String::from_utf8_lossy(&self.src[num_start..self.pos]);
961            match text.parse::<u32>() {
962                Ok(n) if (1..=MAX_VARIABLE_NUMBER).contains(&n) => TokenKind::QuestionNum(n),
963                Ok(n) => TokenKind::Error(format!(
964                    "variable number must be between ?1 and ?{MAX_VARIABLE_NUMBER}, got ?{n}"
965                )),
966                Err(_) => TokenKind::Error("invalid parameter number".to_owned()),
967            }
968        } else {
969            TokenKind::Question
970        }
971    }
972
973    fn lex_alpha_param(&mut self, prefix: char, constructor: fn(String) -> TokenKind) -> TokenKind {
974        self.advance(); // skip prefix
975        let name_start = self.pos;
976        while self.pos < self.src.len() {
977            let ch = self.src[self.pos];
978            if ch.is_ascii_alphanumeric() || ch == b'_' || ch >= 0x80 {
979                self.advance();
980            } else if ch == b':' && self.peek_at(1) == Some(b':') {
981                self.advance();
982                self.advance();
983            } else if ch == b'(' {
984                self.advance();
985                while self.pos < self.src.len() && self.src[self.pos] != b')' {
986                    self.advance();
987                }
988                if self.pos >= self.src.len() || self.src[self.pos] != b')' {
989                    let name = String::from_utf8_lossy(&self.src[name_start..self.pos]);
990                    return TokenKind::Error(format!("unrecognized token: \"{prefix}{name}\""));
991                }
992                self.advance();
993                break; // Tcl array variable parameters end after the closing paren.
994            } else {
995                break;
996            }
997        }
998        if self.pos == name_start {
999            return TokenKind::Error(format!("empty parameter name after '{prefix}'"));
1000        }
1001        let name = String::from_utf8_lossy(&self.src[name_start..self.pos]).into_owned();
1002        constructor(name)
1003    }
1004
1005    /// Lex `:name`.
1006    fn lex_colon_param(&mut self) -> TokenKind {
1007        self.lex_alpha_param(':', TokenKind::ColonParam)
1008    }
1009
1010    /// Lex `@name`.
1011    fn lex_at_param(&mut self) -> TokenKind {
1012        self.lex_alpha_param('@', TokenKind::AtParam)
1013    }
1014
1015    /// Lex `$name`.
1016    fn lex_dollar_param(&mut self) -> TokenKind {
1017        self.lex_alpha_param('$', TokenKind::DollarParam)
1018    }
1019
1020    // -----------------------------------------------------------------------
1021    // Multi-character operator tokenizers
1022    // -----------------------------------------------------------------------
1023
1024    /// Lex `-`, `->`, or `->>`.
1025    fn lex_minus_or_arrow(&mut self) -> TokenKind {
1026        self.advance(); // skip -
1027        if self.peek() == Some(b'>') {
1028            self.advance(); // skip >
1029            if self.peek() == Some(b'>') {
1030                self.advance(); // skip >
1031                TokenKind::DoubleArrow
1032            } else {
1033                TokenKind::Arrow
1034            }
1035        } else {
1036            TokenKind::Minus
1037        }
1038    }
1039
1040    /// Lex `<`, `<=`, `<>`, or `<<`.
1041    fn lex_lt(&mut self) -> TokenKind {
1042        self.advance(); // skip <
1043        match self.peek() {
1044            Some(b'=') => {
1045                self.advance();
1046                TokenKind::Le
1047            }
1048            Some(b'>') => {
1049                self.advance();
1050                TokenKind::LtGt
1051            }
1052            Some(b'<') => {
1053                self.advance();
1054                TokenKind::ShiftLeft
1055            }
1056            _ => TokenKind::Lt,
1057        }
1058    }
1059
1060    /// Lex `>`, `>=`, or `>>`.
1061    fn lex_gt(&mut self) -> TokenKind {
1062        self.advance(); // skip >
1063        match self.peek() {
1064            Some(b'=') => {
1065                self.advance();
1066                TokenKind::Ge
1067            }
1068            Some(b'>') => {
1069                self.advance();
1070                TokenKind::ShiftRight
1071            }
1072            _ => TokenKind::Gt,
1073        }
1074    }
1075
1076    /// Lex `=` or `==`.
1077    fn lex_eq(&mut self) -> TokenKind {
1078        self.advance(); // skip =
1079        if self.peek() == Some(b'=') {
1080            self.advance();
1081            TokenKind::EqEq
1082        } else {
1083            TokenKind::Eq
1084        }
1085    }
1086
1087    /// Lex `!=`.
1088    fn lex_bang(&mut self) -> TokenKind {
1089        self.advance(); // skip !
1090        if self.peek() == Some(b'=') {
1091            self.advance();
1092            TokenKind::Ne
1093        } else {
1094            TokenKind::Error("unexpected '!', did you mean '!='?".to_owned())
1095        }
1096    }
1097
1098    /// Lex `|` or `||`.
1099    fn lex_pipe(&mut self) -> TokenKind {
1100        self.advance(); // skip |
1101        if self.peek() == Some(b'|') {
1102            self.advance();
1103            TokenKind::Concat
1104        } else {
1105            TokenKind::Pipe
1106        }
1107    }
1108}
1109
1110/// Convert an ASCII hex digit byte to its numeric value (0-15).
1111/// Returns `None` for non-hex bytes.
1112const fn hex_digit(b: u8) -> Option<u8> {
1113    match b {
1114        b'0'..=b'9' => Some(b - b'0'),
1115        b'a'..=b'f' => Some(b - b'a' + 10),
1116        b'A'..=b'F' => Some(b - b'A' + 10),
1117        _ => None,
1118    }
1119}
1120
1121#[cfg(test)]
1122mod tests {
1123    use super::*;
1124
1125    fn lex(src: &str) -> Vec<Token> {
1126        Lexer::tokenize(src)
1127    }
1128
1129    fn kinds(src: &str) -> Vec<TokenKind> {
1130        lex(src).into_iter().map(|t| t.kind).collect()
1131    }
1132
1133    #[test]
1134    fn test_lex_integer_literals() {
1135        let tokens = kinds("42 0 0xFF");
1136        assert_eq!(
1137            tokens,
1138            vec![
1139                TokenKind::Integer(42),
1140                TokenKind::Integer(0),
1141                TokenKind::Integer(255),
1142                TokenKind::Eof,
1143            ]
1144        );
1145    }
1146
1147    #[test]
1148    fn test_tokenize_into_reuses_caller_owned_capacity() {
1149        let mut scratch = Vec::new();
1150        Lexer::tokenize_into(
1151            "SELECT 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz';",
1152            &mut scratch,
1153        );
1154        let warmed_capacity = scratch.capacity();
1155        assert!(
1156            warmed_capacity > 0,
1157            "warm parse should allocate token scratch"
1158        );
1159
1160        Lexer::tokenize_into("SELECT 1;", &mut scratch);
1161        assert_eq!(
1162            scratch.capacity(),
1163            warmed_capacity,
1164            "smaller follow-up parse should reuse the warmed token buffer",
1165        );
1166        assert_eq!(
1167            scratch.last().map(|token| &token.kind),
1168            Some(&TokenKind::Eof),
1169            "tokenize_into should still terminate with EOF in reused scratch",
1170        );
1171    }
1172
1173    #[test]
1174    fn test_lex_float_literals() {
1175        let tokens = kinds("3.14 1e10 .5 1.0e-3 0.0");
1176        // Avoid clippy::approx_constant (3.14 is interpreted as an approximation of PI),
1177        // but keep the test input string stable.
1178        let expected = 3.0 + 0.14;
1179        assert!(matches!(
1180            tokens[0],
1181            TokenKind::Float(v) if (v - expected).abs() < 1e-10
1182        ));
1183        assert!(matches!(tokens[1], TokenKind::Float(v) if (v - 1e10).abs() < 1.0));
1184        assert!(matches!(tokens[2], TokenKind::Float(v) if (v - 0.5).abs() < 1e-10));
1185        assert!(matches!(tokens[3], TokenKind::Float(v) if (v - 0.001).abs() < 1e-10));
1186        assert!(matches!(tokens[4], TokenKind::Float(v) if v.abs() < 1e-10));
1187        assert_eq!(tokens[5], TokenKind::Eof);
1188    }
1189
1190    #[test]
1191    fn test_lex_string_literals() {
1192        let tokens = kinds("'hello' 'it''s' ''");
1193        assert_eq!(tokens[0], TokenKind::String("hello".to_owned()));
1194        assert_eq!(tokens[1], TokenKind::String("it's".to_owned()));
1195        assert_eq!(tokens[2], TokenKind::String(String::new()));
1196        assert_eq!(tokens[3], TokenKind::Eof);
1197    }
1198
1199    #[test]
1200    fn test_lex_blob_literals() {
1201        let tokens = kinds("X'CAFE' x'00ff' X''");
1202        assert_eq!(tokens[0], TokenKind::Blob(vec![0xCA, 0xFE]));
1203        assert_eq!(tokens[1], TokenKind::Blob(vec![0x00, 0xFF]));
1204        assert_eq!(tokens[2], TokenKind::Blob(vec![]));
1205        assert_eq!(tokens[3], TokenKind::Eof);
1206    }
1207
1208    #[test]
1209    fn test_lex_blob_odd_hex_error() {
1210        let tokens = kinds("X'CAF'");
1211        assert!(matches!(tokens[0], TokenKind::Error(_)));
1212    }
1213
1214    #[test]
1215    fn test_lex_blob_non_ascii_no_panic() {
1216        // bd-20gf regression: multi-byte UTF-8 inside a blob literal must
1217        // produce an error, not panic on string-slice boundary.
1218        let tokens = kinds("X'U\u{05fc} '");
1219        assert!(matches!(tokens[0], TokenKind::Error(_)));
1220
1221        // Also test with raw non-hex ASCII chars.
1222        let tokens2 = kinds("X'GG'");
1223        assert!(matches!(tokens2[0], TokenKind::Error(_)));
1224    }
1225
1226    #[test]
1227    fn test_lex_variables() {
1228        let tokens = kinds("?1 :name @param $var ?");
1229        assert_eq!(tokens[0], TokenKind::QuestionNum(1));
1230        assert_eq!(tokens[1], TokenKind::ColonParam("name".to_owned()));
1231        assert_eq!(tokens[2], TokenKind::AtParam("param".to_owned()));
1232        assert_eq!(tokens[3], TokenKind::DollarParam("var".to_owned()));
1233        assert_eq!(tokens[4], TokenKind::Question);
1234        assert_eq!(tokens[5], TokenKind::Eof);
1235    }
1236
1237    #[test]
1238    fn test_lex_quoted_identifiers() {
1239        let tokens = kinds("\"table_name\" [column] `backtick`");
1240        assert_eq!(tokens[0], TokenKind::QuotedId("table_name".into(), true));
1241        assert_eq!(tokens[1], TokenKind::QuotedId("column".into(), false));
1242        assert_eq!(tokens[2], TokenKind::QuotedId("backtick".into(), false));
1243    }
1244
1245    #[test]
1246    fn test_lex_dqs_flag() {
1247        let tokens = kinds("\"hello\"");
1248        // Double-quoted strings produce QuotedId with EP_DblQuoted=true
1249        assert_eq!(tokens[0], TokenKind::QuotedId("hello".into(), true));
1250    }
1251
1252    #[test]
1253    fn test_lex_keywords() {
1254        let tokens = kinds("SELECT FROM WHERE INSERT CREATE TABLE CONCURRENT");
1255        assert_eq!(tokens[0], TokenKind::KwSelect);
1256        assert_eq!(tokens[1], TokenKind::KwFrom);
1257        assert_eq!(tokens[2], TokenKind::KwWhere);
1258        assert_eq!(tokens[3], TokenKind::KwInsert);
1259        assert_eq!(tokens[4], TokenKind::KwCreate);
1260        assert_eq!(tokens[5], TokenKind::KwTable);
1261        assert_eq!(tokens[6], TokenKind::KwConcurrent);
1262
1263        // Case insensitivity
1264        let tokens2 = kinds("select from where");
1265        assert_eq!(tokens2[0], TokenKind::KwSelect);
1266        assert_eq!(tokens2[1], TokenKind::KwFrom);
1267        assert_eq!(tokens2[2], TokenKind::KwWhere);
1268    }
1269
1270    #[test]
1271    fn test_lex_operators() {
1272        let tokens = kinds("+ - * / % & | ~ << >> = < <= > >= == != <> || -> ->>");
1273        let expected = vec![
1274            TokenKind::Plus,
1275            TokenKind::Minus,
1276            TokenKind::Star,
1277            TokenKind::Slash,
1278            TokenKind::Percent,
1279            TokenKind::Ampersand,
1280            TokenKind::Pipe,
1281            TokenKind::Tilde,
1282            TokenKind::ShiftLeft,
1283            TokenKind::ShiftRight,
1284            TokenKind::Eq,
1285            TokenKind::Lt,
1286            TokenKind::Le,
1287            TokenKind::Gt,
1288            TokenKind::Ge,
1289            TokenKind::EqEq,
1290            TokenKind::Ne,
1291            TokenKind::LtGt,
1292            TokenKind::Concat,
1293            TokenKind::Arrow,
1294            TokenKind::DoubleArrow,
1295            TokenKind::Eof,
1296        ];
1297        assert_eq!(tokens, expected);
1298    }
1299
1300    #[test]
1301    fn test_lex_eq_vs_eqeq() {
1302        let tokens = kinds("= ==");
1303        assert_eq!(tokens[0], TokenKind::Eq);
1304        assert_eq!(tokens[1], TokenKind::EqEq);
1305    }
1306
1307    #[test]
1308    fn test_lex_ne_vs_ltgt() {
1309        let tokens = kinds("!= <>");
1310        assert_eq!(tokens[0], TokenKind::Ne);
1311        assert_eq!(tokens[1], TokenKind::LtGt);
1312    }
1313
1314    #[test]
1315    fn test_lex_error_unterminated_string() {
1316        let tokens = kinds("'hello");
1317        assert!(matches!(tokens[0], TokenKind::Error(_)));
1318    }
1319
1320    #[test]
1321    fn test_lex_line_column_tracking() {
1322        let tokens = lex("SELECT\n  a,\n  b");
1323        assert_eq!(tokens[0].line, 1);
1324        assert_eq!(tokens[0].col, 1);
1325        // 'a' is on line 2, col 3
1326        assert_eq!(tokens[1].line, 2);
1327        assert_eq!(tokens[1].col, 3);
1328        // ',' is on line 2, col 4
1329        assert_eq!(tokens[2].line, 2);
1330        assert_eq!(tokens[2].col, 4);
1331        // 'b' is on line 3, col 3
1332        assert_eq!(tokens[3].line, 3);
1333        assert_eq!(tokens[3].col, 3);
1334    }
1335
1336    #[test]
1337    fn test_lex_whitespace_and_comments_skipped() {
1338        let tokens = kinds("SELECT -- this is a comment\n  a /* block */ FROM b");
1339        assert_eq!(tokens[0], TokenKind::KwSelect);
1340        assert_eq!(tokens[1], TokenKind::Id("a".into()));
1341        assert_eq!(tokens[2], TokenKind::KwFrom);
1342        assert_eq!(tokens[3], TokenKind::Id("b".into()));
1343        assert_eq!(tokens[4], TokenKind::Eof);
1344    }
1345
1346    #[test]
1347    fn test_lex_hex_large_values() {
1348        // C SQLite parses hex as u64 and memcpy to i64.
1349        // 0xFFFFFFFFFFFFFFFF = u64::MAX → i64 -1.
1350        let tokens = kinds("0xFFFFFFFFFFFFFFFF");
1351        assert_eq!(tokens[0], TokenKind::Integer(-1));
1352
1353        // 0x8000000000000000 = i64::MIN.
1354        let tokens = kinds("0x8000000000000000");
1355        assert_eq!(tokens[0], TokenKind::Integer(i64::MIN));
1356
1357        // 0x7FFFFFFFFFFFFFFF = i64::MAX.
1358        let tokens = kinds("0x7FFFFFFFFFFFFFFF");
1359        assert_eq!(tokens[0], TokenKind::Integer(i64::MAX));
1360    }
1361
1362    #[test]
1363    fn test_lex_hex_overflow_17_digits_rejects() {
1364        // 0x10000000000000000 has 17 significant hex digits → must error,
1365        // not silently truncate to 0.
1366        let tokens = kinds("0x10000000000000000");
1367        assert!(
1368            matches!(&tokens[0], TokenKind::Error(msg) if msg.contains("out of range")),
1369            "expected error for 17-digit hex, got {:?}",
1370            tokens[0]
1371        );
1372    }
1373
1374    #[test]
1375    fn test_lex_hex_leading_zeros_accepted() {
1376        // Leading zeros are stripped before the length check, so
1377        // 0x00000000000000001 (17 chars, 1 significant) is valid.
1378        let tokens = kinds("0x00000000000000001");
1379        assert_eq!(tokens[0], TokenKind::Integer(1));
1380    }
1381
1382    #[test]
1383    fn test_lex_number_hex() {
1384        let tokens = kinds("0x1A 0Xff 0x0");
1385        assert_eq!(tokens[0], TokenKind::Integer(26));
1386        assert_eq!(tokens[1], TokenKind::Integer(255));
1387        assert_eq!(tokens[2], TokenKind::Integer(0));
1388        assert_eq!(tokens[3], TokenKind::Eof);
1389    }
1390
1391    #[test]
1392    fn test_lex_number_unrecognized() {
1393        let tokens = kinds("123a 123.a");
1394        assert!(
1395            matches!(tokens[0], TokenKind::Error(ref e) if e.contains("unrecognized token: \"123a\""))
1396        );
1397        assert!(
1398            matches!(tokens[1], TokenKind::Error(ref e) if e.contains("unrecognized token: \"123.a\""))
1399        );
1400    }
1401
1402    #[test]
1403    fn test_lex_number_hex_invalid() {
1404        let tokens = kinds("0x");
1405        assert!(matches!(tokens[0], TokenKind::Error(_)));
1406    }
1407
1408    #[test]
1409    fn test_lex_positional_params() {
1410        let tokens = kinds("? ?123");
1411        assert_eq!(tokens[0], TokenKind::Question);
1412        assert_eq!(tokens[1], TokenKind::QuestionNum(123));
1413        assert_eq!(tokens[2], TokenKind::Eof);
1414    }
1415
1416    #[test]
1417    fn test_lex_positional_params_reject_zero_and_out_of_range() {
1418        let tokens = kinds("?0 ?32767");
1419        assert!(
1420            matches!(tokens[0], TokenKind::Error(ref e) if e.contains("between ?1 and ?32766")),
1421            "expected ?0 to be rejected, got {:?}",
1422            tokens[0]
1423        );
1424        assert!(
1425            matches!(tokens[1], TokenKind::Error(ref e) if e.contains("between ?1 and ?32766")),
1426            "expected ?32767 to be rejected, got {:?}",
1427            tokens[1]
1428        );
1429        assert_eq!(tokens[2], TokenKind::Eof);
1430    }
1431
1432    #[test]
1433    fn test_lex_named_params() {
1434        let tokens = kinds(":foo @bar $baz_123");
1435        assert_eq!(tokens[0], TokenKind::ColonParam("foo".to_owned()));
1436        assert_eq!(tokens[1], TokenKind::AtParam("bar".to_owned()));
1437        assert_eq!(tokens[2], TokenKind::DollarParam("baz_123".to_owned()));
1438        assert_eq!(tokens[3], TokenKind::Eof);
1439    }
1440
1441    #[test]
1442    fn test_lex_named_params_with_tcl_syntax() {
1443        let tokens = kinds("$::foo(bar) :a::b");
1444        assert_eq!(tokens[0], TokenKind::DollarParam("::foo(bar)".to_owned()));
1445        assert_eq!(tokens[1], TokenKind::ColonParam("a::b".to_owned()));
1446        assert_eq!(tokens[2], TokenKind::Eof);
1447    }
1448
1449    #[test]
1450    fn test_lex_named_params_with_unclosed_tcl_array_syntax() {
1451        let tokens = kinds("$::foo(bar");
1452        assert!(
1453            matches!(tokens[0], TokenKind::Error(ref e) if e.contains("unrecognized token")),
1454            "expected unterminated Tcl-style parameter to be rejected, got {:?}",
1455            tokens[0]
1456        );
1457        assert_eq!(tokens[1], TokenKind::Eof);
1458    }
1459
1460    fn histogram_total(hist: &TokenizeDurationSecondsHistogram) -> u64 {
1461        hist.le_100us + hist.le_250us + hist.le_500us + hist.le_1ms + hist.le_5ms + hist.gt_5ms
1462    }
1463
1464    #[test]
1465    fn test_tokenize_metrics_accumulate_tokens_and_histogram_samples() {
1466        let prev_metrics_enabled = tokenize_metrics_enabled();
1467        reset_tokenize_metrics();
1468        set_tokenize_metrics_enabled(true);
1469
1470        let first = lex("SELECT 1;");
1471        let second = lex("SELECT 2;");
1472
1473        let expected_total_tokens = u64::try_from(first.len() + second.len()).unwrap_or(u64::MAX);
1474        let snap = tokenize_metrics_snapshot();
1475        assert_eq!(snap.fsqlite_tokenize_tokens_total, expected_total_tokens);
1476        assert_eq!(snap.fsqlite_tokenize_duration_seconds_count, 2);
1477        assert_eq!(
1478            histogram_total(&snap.fsqlite_tokenize_duration_seconds),
1479            snap.fsqlite_tokenize_duration_seconds_count
1480        );
1481
1482        set_tokenize_metrics_enabled(prev_metrics_enabled);
1483        reset_tokenize_metrics();
1484    }
1485
1486    #[test]
1487    fn test_tokenize_metrics_reset_clears_all_fields() {
1488        let prev_metrics_enabled = tokenize_metrics_enabled();
1489        reset_tokenize_metrics();
1490        set_tokenize_metrics_enabled(true);
1491        let _ = lex("SELECT 42;");
1492
1493        let before = tokenize_metrics_snapshot();
1494        assert!(before.fsqlite_tokenize_tokens_total > 0);
1495        assert!(before.fsqlite_tokenize_duration_seconds_count > 0);
1496
1497        reset_tokenize_metrics();
1498        let after = tokenize_metrics_snapshot();
1499        assert_eq!(after.fsqlite_tokenize_tokens_total, 0);
1500        assert_eq!(after.fsqlite_tokenize_duration_seconds_count, 0);
1501        assert_eq!(after.fsqlite_tokenize_duration_seconds_sum_micros, 0);
1502        assert_eq!(histogram_total(&after.fsqlite_tokenize_duration_seconds), 0);
1503
1504        set_tokenize_metrics_enabled(prev_metrics_enabled);
1505    }
1506
1507    #[test]
1508    fn test_tokenize_metrics_can_be_disabled_off_hot_path() {
1509        let prev_metrics_enabled = tokenize_metrics_enabled();
1510        reset_tokenize_metrics();
1511        set_tokenize_metrics_enabled(false);
1512
1513        let _ = lex("SELECT 99;");
1514
1515        let snap = tokenize_metrics_snapshot();
1516        assert_eq!(snap.fsqlite_tokenize_tokens_total, 0);
1517        assert_eq!(snap.fsqlite_tokenize_duration_seconds_count, 0);
1518        assert_eq!(snap.fsqlite_tokenize_duration_seconds_sum_micros, 0);
1519        assert_eq!(histogram_total(&snap.fsqlite_tokenize_duration_seconds), 0);
1520
1521        set_tokenize_metrics_enabled(prev_metrics_enabled);
1522        reset_tokenize_metrics();
1523    }
1524}