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