Skip to main content

rudb_parse/
tokenize.rs

1//! The tokenizer, matched to DuckDB's by behaviour.
2//!
3//! This is the one part of the front end with no declarative artifact behind it. The grammar is
4//! vendored and generated from, per `spec/20-the-grammar.md`, and it says nothing about string
5//! literals, dollar quoting, numeric literal forms, comments or the operator rules. All of that
6//! is 613 lines of hand written C++ upstream, in `src/parser/peg/tokenizer/base_tokenizer.cpp`
7//! and `parser_tokenizer.cpp`, and this is a port of it rather than an interpretation.
8//!
9//! Which is worth saying plainly: everything here is a fact about DuckDB and not a design
10//! decision of ours. Where the behaviour looks wrong, it is still the behaviour, because a query
11//! that returns a different answer is worse than a query that returns a surprising one. Section
12//! 20.7 enumerates the ten that bite. The reason a differential fuzzer against a real DuckDB is
13//! scheduled from week one rather than at M5 is this file.
14//!
15//! One deliberate divergence, and it is representational. Upstream types a quoted identifier and
16//! a bare one both as `IDENTIFIER` and recovers the difference from the first byte where it
17//! matters. We give them separate kinds. Nothing downstream may treat them differently in a place
18//! upstream does not.
19//!
20//! Nothing is decoded. A string keeps its quotes and its escapes, a number keeps its underscores,
21//! an identifier keeps its case. `spec/20-the-grammar.md` section 7 for why case in particular:
22//! DuckDB is case insensitive and case preserving, including for quoted identifiers, which is not
23//! what PostgreSQL does and not what folding here would give.
24
25use rudb_common::{Error, Result, Span};
26
27use crate::generated::keywords::{KEYWORDS, LONGEST};
28use crate::token::{Flags, Kind, NOT_A_KEYWORD, Token};
29
30/// Split `query` into tokens, ending with exactly one [`Kind::EndOfInput`].
31///
32/// Fails only where DuckDB's parser tokenizer throws, which is four cases: an unterminated block
33/// comment, an unterminated string literal, an unterminated quoted identifier, and the empty
34/// quoted identifier `""`. An unterminated dollar quoted string is not one of them and comes back
35/// as a token with [`Flags::UNTERMINATED`] set.
36pub fn tokenize(query: &str) -> Result<Vec<Token>> {
37    Ok(Tokenizer::new(query).run()?.0)
38}
39
40/// The body of every `/*+ ... */` hint in `query`, in the order they were written.
41///
42/// A hint is a block comment whose first character is a plus, which is how Oracle, MySQL and Spark
43/// all spell one, and what comes back is the text between the plus and the closing `*/` with
44/// nothing done to it. What a hint means is not a question for the tokenizer: `rudb_seam` reads the
45/// body, through the same function a `SET` goes through.
46///
47/// The tokenizer runs rather than a second scanner over the text, because `/*+` inside a string
48/// literal is not a hint and a second scanner is a second opinion about where a literal ends. It
49/// only runs when the text has `/*+` in it at all, so a query without a hint pays for one substring
50/// search.
51///
52/// # Errors
53///
54/// The four the tokenizer throws, since it is the tokenizer doing the work.
55pub fn hints(query: &str) -> Result<Vec<&str>> {
56    if !query.contains("/*+") {
57        return Ok(Vec::new());
58    }
59    let (_, spans) = Tokenizer::new(query).run()?;
60    Ok(spans.iter().map(|span| &query[span.start as usize..span.end as usize]).collect())
61}
62
63/// Where the scan is.
64///
65/// Named for what upstream calls them so that reading the two side by side stays possible, which
66/// matters more here than anywhere else in the crate.
67#[derive(Clone, Copy, PartialEq, Eq)]
68enum State {
69    Standard,
70    LineComment,
71    BlockComment,
72    QuotedIdentifier,
73    StringLiteral,
74    Word,
75    Numeric,
76    Operator,
77    DollarQuoted,
78}
79
80struct Tokenizer<'a> {
81    query: &'a str,
82    bytes: &'a [u8],
83    tokens: Vec<Token>,
84    /// Where the token being built starts, and after a token is pushed, where the gap starts.
85    last: usize,
86    /// A block comment ended here. Used to set [`Flags::BLOCK_COMMENT`] on the next token, which
87    /// is the whole reason comments are tracked rather than skipped.
88    block_comment_at: Option<usize>,
89    /// The body of each `/*+ ... */`, for [`hints`]. Empty for almost every query there is.
90    hints: Vec<Span>,
91    /// Set by an `E` prefix, which is the only one of the four that changes how the body is read.
92    escape_string: bool,
93    /// The tag between the dollars, as a span, so that closing it is a slice comparison.
94    dollar_tag: Span,
95    depth: u32,
96}
97
98impl<'a> Tokenizer<'a> {
99    fn new(query: &'a str) -> Self {
100        Tokenizer {
101            query,
102            bytes: query.as_bytes(),
103            // Two tokens every seven bytes is what the ClickBench and TPC-H queries come out at.
104            // Being wrong here costs a realloc, being absent costs six.
105            tokens: Vec::with_capacity(query.len() / 4 + 4),
106            last: 0,
107            block_comment_at: None,
108            hints: Vec::new(),
109            escape_string: false,
110            dollar_tag: Span::new(0, 0),
111            depth: 0,
112        }
113    }
114
115    fn run(mut self) -> Result<(Vec<Token>, Vec<Span>)> {
116        let mut state = State::Standard;
117        let mut i = 0;
118        while i < self.bytes.len() {
119            let c = self.bytes[i];
120            match state {
121                State::Standard => {
122                    if let Some(next) = self.standard(&mut i, c)? {
123                        state = next;
124                    }
125                }
126                State::Numeric => self.numeric(&mut state, &mut i, c),
127                State::Operator => self.operator(&mut state, &mut i, c),
128                State::Word => self.word(&mut state, &mut i, c),
129                State::StringLiteral => self.string_literal(&mut state, &mut i, c),
130                State::QuotedIdentifier => self.quoted_identifier(&mut state, &mut i, c)?,
131                State::LineComment => {
132                    if c == b'\n' || c == b'\r' {
133                        self.comment(self.last, i + 1);
134                        self.last = i + 1;
135                        state = State::Standard;
136                    }
137                }
138                State::BlockComment => self.block_comment(&mut state, &mut i, c),
139                State::DollarQuoted => self.dollar_quoted(&mut state, &mut i),
140            }
141            i += 1;
142        }
143        self.finish(state)
144    }
145
146    /// The end of the input, which is a different decision per state and not a loop exit.
147    fn finish(mut self, state: State) -> Result<(Vec<Token>, Vec<Span>)> {
148        let end = self.bytes.len();
149        match state {
150            State::LineComment => {
151                self.comment(self.last, end);
152            }
153            State::BlockComment => {
154                return Err(self.error(
155                    format!(
156                        "unterminated /* comment at or near \"{}\"",
157                        &self.query[self.last..end]
158                    ),
159                    self.last,
160                ));
161            }
162            State::Operator => self.push_operator(self.last, end),
163            State::DollarQuoted => {
164                // Not an error upstream, which is worth noticing rather than tidying up. It comes
165                // back as a token that ran off the end and the grammar gets to decide.
166                self.push_flagged(self.last, end, Kind::String, Flags::UNTERMINATED);
167            }
168            State::StringLiteral => {
169                return Err(self.error("unterminated string literal", self.last));
170            }
171            State::QuotedIdentifier => {
172                return Err(self.error("unterminated quoted identifier", self.last));
173            }
174            State::Numeric => self.push(self.last, end, Kind::Number),
175            State::Word => self.push_word(self.last, end),
176            // A `$` with nothing after it lands here, and upstream calls it an identifier. It is
177            // reproduced rather than corrected, because a tokenizer that disagrees with DuckDB
178            // about a one byte query disagrees with it about something.
179            State::Standard => self.push(self.last, end, Kind::Identifier),
180        }
181        self.tokens.push(Token {
182            kind: Kind::EndOfInput,
183            flags: Flags::default(),
184            keyword: NOT_A_KEYWORD,
185            start: end as u32,
186            end: end as u32,
187        });
188        Ok((self.tokens, self.hints))
189    }
190
191    /// The dispatch at the top of a token. Returns the state to move to, if it changes.
192    fn standard(&mut self, i: &mut usize, c: u8) -> Result<Option<State>> {
193        match c {
194            b'\'' => {
195                self.last = *i;
196                self.escape_string = false;
197                return Ok(Some(State::StringLiteral));
198            }
199            b'"' => {
200                self.last = *i;
201                return Ok(Some(State::QuotedIdentifier));
202            }
203            b';' => {
204                // The base tokenizer emits nothing here and lets its caller decide. The parser's
205                // caller emits `;`, because `Program <- TopLevelStatement*` consumes it. The
206                // autocomplete caller does something else, which is why the hook exists.
207                self.tokens.push(Token {
208                    kind: Kind::Terminator,
209                    flags: self.gap_flags(*i),
210                    keyword: NOT_A_KEYWORD,
211                    start: *i as u32,
212                    end: *i as u32 + 1,
213                });
214                self.last = *i + 1;
215                return Ok(None);
216            }
217            b'$' => return Ok(self.dollar(i)),
218            b'-' if self.bytes.get(*i + 1) == Some(&b'-') => {
219                *i += 1;
220                return Ok(Some(State::LineComment));
221            }
222            b'/' if self.bytes.get(*i + 1) == Some(&b'*') => {
223                *i += 1;
224                self.depth = 1;
225                return Ok(Some(State::BlockComment));
226            }
227            _ => {}
228        }
229
230        if is_space(c) {
231            self.last = *i + 1;
232            return Ok(None);
233        }
234
235        if let Some(len) = special_operator(self.bytes, *i) {
236            // `::=` is an operator run and not `::` followed by `=`. The three character check
237            // above is why `->` survives the rule that a `-` never joins anything.
238            if self.bytes.get(*i + len).is_some_and(|&next| is_operator_char_in_run(next)) {
239                self.last = *i;
240                return Ok(Some(State::Operator));
241            }
242            self.push(*i, *i + len, Kind::Operator);
243            *i += len - 1;
244            self.last = *i + 1;
245            return Ok(None);
246        }
247
248        if is_single_byte_operator(c) {
249            self.push(*i, *i + 1, Kind::Operator);
250            self.last = *i + 1;
251            return Ok(None);
252        }
253
254        if is_initial_number(c) {
255            self.last = *i;
256            return Ok(Some(State::Numeric));
257        }
258
259        // `E`, `X`, `B` and `N`, in either case, and only when the quote is the very next byte.
260        // `SELECT e 'a'` is an identifier and then a string. Only `E` changes how the body is
261        // read, and it is the only one of the four this tokenizer does anything else with.
262        if is_string_prefix(c) && self.bytes.get(*i + 1) == Some(&b'\'') {
263            self.last = *i;
264            self.escape_string = c == b'E' || c == b'e';
265            *i += 1;
266            return Ok(Some(State::StringLiteral));
267        }
268
269        if is_operator_char(c) {
270            self.last = *i;
271            return Ok(Some(State::Operator));
272        }
273
274        self.last = *i;
275        Ok(Some(State::Word))
276    }
277
278    /// `$` is three things and which one it is depends on what follows.
279    fn dollar(&mut self, i: &mut usize) -> Option<State> {
280        let Some(&next) = self.bytes.get(*i + 1) else {
281            // Nothing after it, and upstream leaves `last` where it is, so this byte ends up in
282            // whatever the final token turns out to be.
283            return None;
284        };
285        if next.is_ascii_digit() {
286            // `$1` is a parameter, and it is two tokens rather than one. The grammar spells it,
287            // which is why the tokenizer does not have to.
288            self.push(*i, *i + 1, Kind::Operator);
289            return None;
290        }
291
292        // A tag runs to the next `$` and may contain only tag characters. Anything else and this
293        // was `$name`, which is also two tokens.
294        let mut close = None;
295        for at in *i + 1..self.bytes.len() {
296            if self.bytes[at] == b'$' {
297                close = Some(at);
298                break;
299            }
300            if !is_dollar_tag_char(self.bytes[at]) {
301                break;
302            }
303        }
304        let Some(close) = close else {
305            self.push(*i, *i + 1, Kind::Operator);
306            return None;
307        };
308
309        self.last = *i;
310        self.dollar_tag = Span::new(*i as u32 + 1, close as u32);
311        *i = close;
312        Some(State::DollarQuoted)
313    }
314
315    fn numeric(&mut self, state: &mut State, i: &mut usize, c: u8) {
316        if is_initial_number(c) {
317            return;
318        }
319        // Only between two digits, which is why `1_000` is a thousand and `SELECT 1_` is `1`
320        // aliased `_`.
321        if c == b'_' && self.bytes.get(*i + 1).is_some_and(|&n| is_initial_number(n)) {
322            return;
323        }
324        if is_scientific(c) && !is_scientific(self.bytes[*i - 1]) {
325            // A digit has to be in there somewhere, which rules out `.e100` while allowing both
326            // `1e5` and `.1e5`.
327            if self.bytes[self.last].is_ascii_digit() || self.bytes[*i - 1].is_ascii_digit() {
328                return;
329            }
330        }
331        if (c == b'+' || c == b'-') && is_scientific(self.bytes[*i - 1]) {
332            return;
333        }
334
335        // Give back anything on the end that is not a digit or a dot, which is what turns the `e`
336        // of `1e+` back into something the next pass has to deal with.
337        while !is_initial_number(self.bytes[*i - 1]) {
338            *i -= 1;
339        }
340        self.push(self.last, *i, Kind::Number);
341        *state = State::Standard;
342        self.last = *i;
343        *i -= 1;
344    }
345
346    fn operator(&mut self, state: &mut State, i: &mut usize, c: u8) {
347        if c == b'/' && self.bytes.get(*i + 1) == Some(&b'*') {
348            self.push_operator(self.last, *i);
349            *state = State::Standard;
350            self.last = *i;
351            *i -= 1;
352            return;
353        }
354        if !is_operator_char_in_run(c) {
355            self.push_operator(self.last, *i);
356            *state = State::Standard;
357            self.last = *i;
358            *i -= 1;
359        }
360    }
361
362    fn word(&mut self, state: &mut State, i: &mut usize, c: u8) {
363        // `$` is a legal non-initial identifier character, which is PostgreSQL's rule and is why
364        // this one test is not part of `is_word_char`.
365        if c == b'$' || is_word_char(c) {
366            return;
367        }
368        self.push_word(self.last, *i);
369        *state = State::Standard;
370        self.last = *i;
371        *i -= 1;
372    }
373
374    fn string_literal(&mut self, state: &mut State, i: &mut usize, c: u8) {
375        if self.escape_string && c == b'\\' && *i + 1 < self.bytes.len() {
376            *i += 1;
377            return;
378        }
379        if c != b'\'' {
380            return;
381        }
382        if self.bytes.get(*i + 1) == Some(&b'\'') {
383            *i += 1;
384            return;
385        }
386        self.push(self.last, *i + 1, Kind::String);
387        self.last = *i + 1;
388        self.escape_string = false;
389        *state = State::Standard;
390    }
391
392    fn quoted_identifier(&mut self, state: &mut State, i: &mut usize, c: u8) -> Result<()> {
393        if c != b'"' {
394            return Ok(());
395        }
396        if self.bytes.get(*i + 1) == Some(&b'"') {
397            *i += 1;
398            return Ok(());
399        }
400        if *i + 1 == self.last + 2 {
401            return Err(self.error("zero-length delimited identifier", self.last));
402        }
403        self.push(self.last, *i + 1, Kind::QuotedIdentifier);
404        self.last = *i + 1;
405        *state = State::Standard;
406        Ok(())
407    }
408
409    fn block_comment(&mut self, state: &mut State, i: &mut usize, c: u8) {
410        // Nested, which is the difference between commenting out a block that contains a comment
411        // and getting a syntax error halfway down the file.
412        if c == b'/' && self.bytes.get(*i + 1) == Some(&b'*') {
413            *i += 1;
414            self.depth += 1;
415        } else if c == b'*' && self.bytes.get(*i + 1) == Some(&b'/') {
416            *i += 1;
417            self.depth -= 1;
418            if self.depth == 0 {
419                self.comment(self.last, *i + 1);
420                self.last = *i + 1;
421                *state = State::Standard;
422            }
423        }
424    }
425
426    fn dollar_quoted(&mut self, state: &mut State, i: &mut usize) {
427        if self.bytes[*i] != b'$' || *i + 1 >= self.bytes.len() {
428            return;
429        }
430        let start = *i + 1;
431        let mut end = start;
432        while end < self.bytes.len() && self.bytes[end] != b'$' {
433            end += 1;
434        }
435        if end >= self.bytes.len() {
436            return;
437        }
438        let tag = &self.bytes[self.dollar_tag.start as usize..self.dollar_tag.end as usize];
439        if end - start != tag.len() || &self.bytes[start..end] != tag {
440            return;
441        }
442        self.push(self.last, end + 1, Kind::String);
443        *state = State::Standard;
444        *i = end;
445        self.last = *i + 1;
446    }
447
448    /// Push a bare word, having decided whether it is a keyword.
449    ///
450    /// A word is a keyword when it is in at least one class, which is not the same as being in
451    /// the table. The 15 soft words are in the table with a mask of zero and are identifiers here,
452    /// exactly as `PEGKeywordHelper::IsKeyword` has it, because their lists are the five class
453    /// lists and a soft word is in none of them. They still keep their index, so `ORDER BY x
454    /// ASCENDING` can match the literal without `SELECT ascending FROM t` becoming a syntax error.
455    /// `spec/20-the-grammar.md` section 5.
456    fn push_word(&mut self, start: usize, end: usize) {
457        if start >= end {
458            return;
459        }
460        let keyword = lookup(&self.query[start..end]);
461        let kind = if classes(keyword) == 0 { Kind::Identifier } else { Kind::Keyword };
462        let flags = self.gap_flags(start);
463        self.tokens.push(Token { kind, flags, keyword, start: start as u32, end: end as u32 });
464    }
465
466    /// An operator run, minus a trailing `+` where PostgreSQL says to give it back.
467    ///
468    /// `SELECT 1 =+ 1` is `1 = +1`, and `SELECT 1 !=+ 1` goes looking for an operator named `!=+`,
469    /// because the run in the second case contains a character from the special set. The rule is
470    /// PostgreSQL's and it exists so that a user defined operator can be told apart from an
471    /// operator followed by a signed number.
472    fn push_operator(&mut self, start: usize, end: usize) {
473        let special = self.bytes[start..end].iter().any(|&b| {
474            matches!(b, b'~' | b'!' | b'@' | b'#' | b'%' | b'^' | b'&' | b'|' | b'`' | b'?')
475        });
476        let mut cut = end;
477        if !special {
478            while cut > start && self.bytes[cut - 1] == b'+' {
479                cut -= 1;
480            }
481        }
482        self.push(start, cut, Kind::Operator);
483        for at in cut..end {
484            self.push(at, at + 1, Kind::Operator);
485        }
486    }
487
488    /// Record a comment. Nothing is pushed, because a comment is not a token anywhere the parser
489    /// can see, but where the block ones were has to be remembered so the next token can say it
490    /// was preceded by one.
491    ///
492    /// A block comment that opens `/*+` is a hint and its body is kept as well. It stays a comment
493    /// in every other respect, so a query that hints something DuckDB has never heard of parses
494    /// there too, which is what makes a hint safe to leave in a file two engines read.
495    fn comment(&mut self, start: usize, end: usize) {
496        if end >= start + 2 && &self.bytes[start..start + 2] == b"/*" {
497            self.block_comment_at = Some(start);
498            if end >= start + 5 && self.bytes[start + 2] == b'+' {
499                self.hints.push(Span::new(start as u32 + 3, end as u32 - 2));
500            }
501        }
502    }
503
504    /// Push a token that is not a bare word, unless it is empty.
505    ///
506    /// The empty check is upstream's and it is what lets several states push unconditionally at a
507    /// boundary without first asking whether they have anything.
508    ///
509    /// One divergence, and it is in a field rather than in a token. Upstream reaches around
510    /// `PushToken` for single byte operators, special operators, a trimmed `+`, a `$` and a `;`,
511    /// so those five arrive with both gap flags clear no matter what was in the gap. We set them
512    /// on everything. Nothing reads a gap flag on an operator today, and a rule that holds for
513    /// every token is one fewer thing to remember when something starts to.
514    fn push(&mut self, start: usize, end: usize, kind: Kind) {
515        if start >= end {
516            return;
517        }
518        let flags = self.gap_flags(start);
519        self.tokens.push(Token {
520            kind,
521            flags,
522            keyword: NOT_A_KEYWORD,
523            start: start as u32,
524            end: end as u32,
525        });
526    }
527
528    fn push_flagged(&mut self, start: usize, end: usize, kind: Kind, extra: Flags) {
529        self.push(start, end, kind);
530        if let Some(token) = self.tokens.last_mut() {
531            token.flags = token.flags.with(extra);
532        }
533    }
534
535    /// What was in the gap between the previous token and `start`.
536    ///
537    /// Two literals separated by whitespace containing a newline are one literal, a line comment
538    /// between them keeps the join, and a block comment breaks it. That rule is PostgreSQL's,
539    /// DuckDB kept it, and it is the only reason either flag exists.
540    fn gap_flags(&self, start: usize) -> Flags {
541        let Some(previous) = self.tokens.last() else { return Flags::default() };
542        let from = previous.end as usize;
543        let mut flags = Flags::default();
544        if self.block_comment_at.is_some_and(|at| at >= from && at < start) {
545            flags = flags.with(Flags::BLOCK_COMMENT);
546        }
547        if self.bytes[from..start.min(self.bytes.len())].iter().any(|&b| b == b'\n' || b == b'\r') {
548            flags = flags.with(Flags::NEWLINE);
549        }
550        flags
551    }
552
553    fn error(&self, message: impl Into<String>, at: usize) -> Error {
554        Error::parser(message).with_span(Span::new(at as u32, self.bytes.len() as u32))
555    }
556}
557
558/// The index of `word` in the generated keyword table, or [`NOT_A_KEYWORD`].
559///
560/// ASCII folded into a fixed buffer, because every keyword is a plain ASCII word and the longest
561/// is 15 bytes, so a longer candidate cannot be one and never touches the table.
562pub fn lookup(word: &str) -> u16 {
563    if word.len() > LONGEST {
564        return NOT_A_KEYWORD;
565    }
566    let mut folded = [0u8; LONGEST];
567    for (slot, byte) in folded.iter_mut().zip(word.bytes()) {
568        *slot = byte.to_ascii_lowercase();
569    }
570    let folded = &folded[..word.len()];
571    // The table is sorted, so this is one binary search over 514 entries, which is nine
572    // comparisons and fits in a handful of cache lines.
573    match KEYWORDS.binary_search_by(|(candidate, _)| candidate.as_bytes().cmp(folded)) {
574        Ok(at) => at as u16,
575        Err(_) => NOT_A_KEYWORD,
576    }
577}
578
579/// The classes `word` belongs to, or zero.
580///
581/// Zero for a word that is in the table with no class, which is one of the 15 soft words, and
582/// zero for a word that is not in the table at all. Those two are the same answer to the only
583/// question this function is asked, which is whether the word blocks an identifier here.
584pub fn classes(keyword: u16) -> u8 {
585    if keyword == NOT_A_KEYWORD { 0 } else { KEYWORDS[keyword as usize].1 }
586}
587
588const fn is_space(c: u8) -> bool {
589    matches!(c, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r')
590}
591
592/// The dozen characters that are always their own token and never join a run.
593///
594/// `-` and `#` being in here is the surprise. It is why `SELECT 1 =- 1` is `1 = -1` and why `--`
595/// can be a comment without ever having to be told it is not an operator.
596const fn is_single_byte_operator(c: u8) -> bool {
597    matches!(c, b'(' | b')' | b'{' | b'}' | b'[' | b']' | b',' | b'?' | b'$' | b'-' | b'#')
598}
599
600/// PostgreSQL's operator character set, which is every ASCII punctuation character except `_`.
601const fn is_operator_char(c: u8) -> bool {
602    if c == b'_' {
603        return false;
604    }
605    matches!(c, b'!'..=b'/' | b':'..=b'@' | b'['..=b'`' | b'{'..=b'~')
606}
607
608/// Whether `c` can continue an operator run.
609///
610/// The single byte operators are excluded, and so are the five characters that always end one:
611/// `'`, `-`, `;`, `"` and `.`. `-` is in both lists and that is not redundant, since the first
612/// list is also consulted where the second is not.
613const fn is_operator_char_in_run(c: u8) -> bool {
614    if is_single_byte_operator(c) || is_control_flow(c) {
615        return false;
616    }
617    is_operator_char(c)
618}
619
620const fn is_control_flow(c: u8) -> bool {
621    matches!(c, b'\'' | b'-' | b';' | b'"' | b'.')
622}
623
624/// Whether `c` can continue a bare word.
625///
626/// Anything that is not punctuation, whitespace or a delimiter, which by the arithmetic above
627/// includes every byte at or above 0x80. So an identifier may be any UTF-8 the user likes, and
628/// the tokenizer never has to decode it to find that out.
629const fn is_word_char(c: u8) -> bool {
630    if is_single_byte_operator(c) || is_operator_char(c) || is_space(c) || is_control_flow(c) {
631        return false;
632    }
633    true
634}
635
636/// A digit or a dot, which is both what starts a number and what can appear anywhere in one.
637///
638/// A dot being unconditional here is why `SELECT 1.2.3` is a single number token rather than
639/// three things. What it means is somebody else's problem, which is exactly how upstream has it.
640const fn is_initial_number(c: u8) -> bool {
641    c.is_ascii_digit() || c == b'.'
642}
643
644const fn is_scientific(c: u8) -> bool {
645    c == b'e' || c == b'E'
646}
647
648const fn is_string_prefix(c: u8) -> bool {
649    matches!(c, b'N' | b'n' | b'X' | b'x' | b'E' | b'e' | b'B' | b'b')
650}
651
652/// A-Z, a-z, 0-9, `_`, and anything at or above 0x80. Digits are only legal after the first byte
653/// and the caller checks that.
654const fn is_dollar_tag_char(c: u8) -> bool {
655    c.is_ascii_alphanumeric() || c == b'_' || c >= 0x80
656}
657
658/// The six sequences checked before the maximal run, longest first.
659///
660/// `->` is here because `-` is a single byte operator and would otherwise never join anything,
661/// which would leave the JSON arrow unspellable.
662fn special_operator(bytes: &[u8], at: usize) -> Option<usize> {
663    if bytes[at..].starts_with(b"->>") {
664        return Some(3);
665    }
666    for candidate in [b"::".as_slice(), b":=", b"->", b"**", b"//"] {
667        if bytes[at..].starts_with(candidate) {
668            return Some(2);
669        }
670    }
671    None
672}
673
674#[cfg(test)]
675mod tests {
676    use super::{classes, lookup, tokenize};
677    use crate::generated::keywords::{RESERVED, UNRESERVED};
678    use crate::token::{Flags, Kind, NOT_A_KEYWORD, Token};
679
680    /// Every token but the sentinel, as the pair worth asserting on.
681    fn scan(query: &str) -> Vec<(Kind, &str)> {
682        let tokens = tokenize(query).expect("tokenizes");
683        assert_eq!(tokens.last().map(|t| t.kind), Some(Kind::EndOfInput));
684        assert_eq!(tokens.iter().filter(|t| t.kind == Kind::EndOfInput).count(), 1);
685        tokens[..tokens.len() - 1].iter().map(|t| (t.kind, t.text(query))).collect()
686    }
687
688    fn texts(query: &str) -> Vec<&str> {
689        scan(query).into_iter().map(|(_, text)| text).collect()
690    }
691
692    fn all(query: &str) -> Vec<Token> {
693        tokenize(query).expect("tokenizes")
694    }
695
696    fn message(query: &str) -> String {
697        tokenize(query).expect_err("fails").message().to_string()
698    }
699
700    #[test]
701    fn the_empty_query_is_one_sentinel() {
702        let tokens = tokenize("").expect("tokenizes");
703        assert_eq!(tokens.len(), 1);
704        assert_eq!(tokens[0].kind, Kind::EndOfInput);
705        assert_eq!(tokens[0].span(), rudb_common::Span::new(0, 0));
706        assert!(scan("   \t\n  ").is_empty());
707    }
708
709    #[test]
710    fn a_word_in_a_class_is_a_keyword_and_one_in_none_is_not() {
711        assert_eq!(scan("SELECT"), [(Kind::Keyword, "SELECT")]);
712        assert_eq!(scan("banana"), [(Kind::Identifier, "banana")]);
713    }
714
715    #[test]
716    fn case_is_matched_but_not_folded() {
717        // DuckDB is case insensitive and case preserving, and unlike PostgreSQL that holds for
718        // quoted identifiers too. So the keyword is recognized whatever its case and the bytes
719        // come back exactly as written. Folding here would be the wrong answer twice over.
720        for spelling in ["select", "SELECT", "SeLeCt"] {
721            let tokens = all(spelling);
722            assert_eq!(tokens[0].kind, Kind::Keyword);
723            assert_eq!(tokens[0].text(spelling), spelling);
724            assert_eq!(tokens[0].keyword, lookup("select"));
725        }
726        assert_eq!(scan(r#""Foo""#), [(Kind::QuotedIdentifier, r#""Foo""#)]);
727    }
728
729    #[test]
730    fn a_soft_word_keeps_its_index_and_stays_an_identifier() {
731        // ASCENDING is spelled by a rule and is in none of the five lists. If it came back as a
732        // keyword then `SELECT ascending FROM t` would stop working, and if it came back without
733        // an index then `ORDER BY x ASCENDING` could not be filtered on the literal.
734        let tokens = all("ascending");
735        assert_eq!(tokens[0].kind, Kind::Identifier);
736        assert_ne!(tokens[0].keyword, NOT_A_KEYWORD);
737        assert_eq!(classes(tokens[0].keyword), 0);
738
739        let tokens = all("banana");
740        assert_eq!(tokens[0].keyword, NOT_A_KEYWORD);
741        assert_eq!(classes(tokens[0].keyword), 0);
742    }
743
744    #[test]
745    fn the_classes_come_back_off_the_index() {
746        assert_eq!(classes(lookup("select")) & RESERVED, RESERVED);
747        assert_eq!(classes(lookup("abort")) & UNRESERVED, UNRESERVED);
748        assert_eq!(lookup("supercalifragilistic"), NOT_A_KEYWORD);
749        assert_eq!(lookup(""), NOT_A_KEYWORD);
750    }
751
752    #[test]
753    fn a_quoted_identifier_is_never_a_keyword() {
754        let tokens = all(r#""select""#);
755        assert_eq!(tokens[0].kind, Kind::QuotedIdentifier);
756        assert_eq!(tokens[0].keyword, NOT_A_KEYWORD);
757        assert_eq!(scan(r#""a""b""#), [(Kind::QuotedIdentifier, r#""a""b""#)]);
758    }
759
760    #[test]
761    fn a_dollar_is_an_identifier_character_after_the_first_byte() {
762        assert_eq!(scan("a$b"), [(Kind::Identifier, "a$b")]);
763    }
764
765    #[test]
766    fn any_byte_above_ascii_is_an_identifier_character() {
767        // Nothing decodes UTF-8 here. The arithmetic in `is_word_char` says every byte at or
768        // above 0x80 continues a word, which is how an identifier in any script gets through
769        // without the tokenizer knowing what script it is.
770        assert_eq!(scan("SELECT café"), [(Kind::Keyword, "SELECT"), (Kind::Identifier, "café")]);
771    }
772
773    #[test]
774    fn a_number_swallows_more_than_a_number() {
775        // Every one of these is a single NUMBER token upstream and the parser deals with what it
776        // means. `1.2.3` in particular is not three things.
777        assert_eq!(scan("1.2.3"), [(Kind::Number, "1.2.3")]);
778        assert_eq!(scan("1_000"), [(Kind::Number, "1_000")]);
779        assert_eq!(scan("1e5"), [(Kind::Number, "1e5")]);
780        assert_eq!(scan(".1e5"), [(Kind::Number, ".1e5")]);
781        assert_eq!(scan("1e-5"), [(Kind::Number, "1e-5")]);
782        assert_eq!(scan("1.e5"), [(Kind::Number, "1.e5")]);
783    }
784
785    #[test]
786    fn a_trailing_e_stays_on_the_number() {
787        // `SELECT 1e` is one NUMBER token "1e" and not `1` aliased `e`, because the tokenizer
788        // takes the `e` and only the parser is in a position to object.
789        assert_eq!(scan("SELECT 1e"), [(Kind::Keyword, "SELECT"), (Kind::Number, "1e")]);
790        assert_eq!(scan("SELECT 1e+"), [(Kind::Keyword, "SELECT"), (Kind::Number, "1e+")]);
791    }
792
793    #[test]
794    fn what_the_number_cannot_use_it_gives_back() {
795        // The backtrack at the end of NUMERIC only keeps digits and dots, so `1e+ 1` unwinds the
796        // whole exponent it started and the `e` comes back as a name.
797        assert_eq!(
798            scan("SELECT 1e+ 1"),
799            [
800                (Kind::Keyword, "SELECT"),
801                (Kind::Number, "1"),
802                (Kind::Identifier, "e"),
803                (Kind::Operator, "+"),
804                (Kind::Number, "1"),
805            ]
806        );
807        assert_eq!(scan("1_"), [(Kind::Number, "1"), (Kind::Identifier, "_")]);
808        assert_eq!(scan("1__0"), [(Kind::Number, "1"), (Kind::Identifier, "__0")]);
809        // There is no hex literal. `0x1F` is zero and then a name, which is worth knowing before
810        // somebody reports it as a bug in the parser.
811        assert_eq!(scan("0x1F"), [(Kind::Number, "0"), (Kind::Identifier, "x1F")]);
812        assert_eq!(scan(".e100"), [(Kind::Number, "."), (Kind::Identifier, "e100")]);
813    }
814
815    #[test]
816    fn a_minus_never_joins_an_operator_run() {
817        // `-` is a single byte operator, which is what makes `SELECT 1 =- 1` a subtraction of a
818        // negative one rather than a call to an operator named `=-`.
819        assert_eq!(texts("1-1"), ["1", "-", "1"]);
820        assert_eq!(texts("SELECT 1 =- 1"), ["SELECT", "1", "=", "-", "1"]);
821        assert_eq!(texts("(a,b)"), ["(", "a", ",", "b", ")"]);
822    }
823
824    #[test]
825    fn the_postgres_plus_rule_decides_where_a_run_ends() {
826        // An operator cannot end in `+` unless it contains one of ~ ! @ # % ^ & | ` ?. This is
827        // the rule that lets a user defined operator be told apart from an operator and a sign.
828        assert_eq!(texts("SELECT 1 =+ 1"), ["SELECT", "1", "=", "+", "1"]);
829        assert_eq!(texts("SELECT 1 !=+ 1"), ["SELECT", "1", "!=+", "1"]);
830        assert_eq!(texts("SELECT 1 =++ 1"), ["SELECT", "1", "=", "+", "+", "1"]);
831        assert_eq!(texts("SELECT 1 ++ 1"), ["SELECT", "1", "+", "+", "1"]);
832    }
833
834    #[test]
835    fn the_special_operators_are_checked_before_the_run() {
836        assert_eq!(texts("a->>'b'"), ["a", "->>", "'b'"]);
837        assert_eq!(texts("a->'b'"), ["a", "->", "'b'"]);
838        assert_eq!(texts("a::b"), ["a", "::", "b"]);
839        assert_eq!(texts("a//b"), ["a", "//", "b"]);
840        assert_eq!(texts("2**3"), ["2", "**", "3"]);
841        // But only when what follows is not itself an operator character, in which case the
842        // maximal run wins and it is one token.
843        assert_eq!(texts("a::=b"), ["a", "::=", "b"]);
844    }
845
846    #[test]
847    fn a_block_comment_can_end_an_operator_run() {
848        assert_eq!(texts("1+/*c*/2"), ["1", "+", "2"]);
849    }
850
851    #[test]
852    fn a_comment_is_not_a_token_but_a_block_one_leaves_a_mark() {
853        assert_eq!(texts("SELECT --x\n1"), ["SELECT", "1"]);
854        assert_eq!(texts("SELECT /*x*/ 1"), ["SELECT", "1"]);
855        assert_eq!(texts("SELECT --x"), ["SELECT"]);
856
857        let tokens = all("SELECT /*x*/ 1");
858        assert!(tokens[1].flags.has(Flags::BLOCK_COMMENT));
859        assert!(!tokens[1].flags.has(Flags::NEWLINE));
860
861        // A line comment is not a block comment, and the newline that ends it still counts.
862        let tokens = all("SELECT --x\n1");
863        assert!(!tokens[1].flags.has(Flags::BLOCK_COMMENT));
864        assert!(tokens[1].flags.has(Flags::NEWLINE));
865    }
866
867    #[test]
868    fn a_hint_is_a_comment_that_can_be_read_back() {
869        assert_eq!(
870            super::hints("SELECT /*+ hash.table(unchained) */ 1").unwrap(),
871            [" hash.table(unchained) "]
872        );
873        // Still a comment, so the tokens are what they would have been without it.
874        assert_eq!(texts("SELECT /*+ hash.table(unchained) */ 1"), ["SELECT", "1"]);
875
876        // Two of them, in the order they were written, from anywhere in the statement.
877        assert_eq!(super::hints("SELECT /*+ a(b) */ 1 /*+ c(d) */").unwrap(), [" a(b) ", " c(d) "]);
878
879        // A comment without the plus is not a hint, and neither is one inside a string literal,
880        // which is the case a scanner that did not know about literals would get wrong.
881        assert!(super::hints("SELECT /* hash.table(unchained) */ 1").unwrap().is_empty());
882        assert!(super::hints("SELECT '/*+ hash.table(unchained) */'").unwrap().is_empty());
883        assert!(super::hints("SELECT 1").unwrap().is_empty());
884    }
885
886    #[test]
887    fn the_first_token_is_preceded_by_nothing() {
888        let tokens = all("\n/*x*/ SELECT");
889        assert_eq!(tokens[0].flags, Flags::default());
890    }
891
892    #[test]
893    fn block_comments_nest() {
894        // The reason this matters is commenting out a block that already contains a comment. In
895        // PostgreSQL it works, in most SQL dialects it does not, and DuckDB followed PostgreSQL.
896        assert_eq!(texts("SELECT /* a /* b */ c */ 1"), ["SELECT", "1"]);
897        assert_eq!(
898            message("SELECT /* a /* b */ 1"),
899            "unterminated /* comment at or near \"/* a /* b */ 1\""
900        );
901    }
902
903    #[test]
904    fn a_string_keeps_its_quotes_and_its_escapes() {
905        assert_eq!(scan("'it''s'"), [(Kind::String, "'it''s'")]);
906        assert_eq!(scan("''"), [(Kind::String, "''")]);
907        assert_eq!(texts("'a' 'b'"), ["'a'", "'b'"]);
908    }
909
910    #[test]
911    fn only_the_e_prefix_changes_how_a_string_is_read() {
912        // In an E string a backslash escapes the next byte, so the doubled quote at the end is
913        // one escaped quote and then the close. Without the prefix the backslash is an ordinary
914        // character, the doubled quote is an escape, and the literal runs off the end.
915        assert_eq!(scan(r"E'\''"), [(Kind::String, r"E'\''")]);
916        assert_eq!(message(r"'\''"), "unterminated string literal");
917        for prefix in ["X", "x", "B", "b", "N", "n", "E", "e"] {
918            let query = format!("{prefix}'a'");
919            assert_eq!(tokenize(&query).expect("tokenizes")[0].kind, Kind::String);
920        }
921        // The quote has to be the very next byte, otherwise it is a name and then a string.
922        assert_eq!(texts("x 'a'"), ["x", "'a'"]);
923    }
924
925    #[test]
926    fn a_dollar_quoted_string_is_one_token_and_its_tag_has_to_match() {
927        assert_eq!(scan("$$abc$$"), [(Kind::String, "$$abc$$")]);
928        assert_eq!(scan("$tag$abc$tag$"), [(Kind::String, "$tag$abc$tag$")]);
929        assert_eq!(scan("$tag$a$other$b$tag$"), [(Kind::String, "$tag$a$other$b$tag$")]);
930        assert_eq!(scan("$$it's fine$$"), [(Kind::String, "$$it's fine$$")]);
931    }
932
933    #[test]
934    fn an_unterminated_dollar_quote_is_a_token_and_not_an_error() {
935        // The other three unterminated forms throw. This one does not, which is upstream's
936        // choice and not ours, and the flag is how the difference reaches the matcher.
937        let tokens = all("$$abc");
938        assert_eq!(tokens[0].kind, Kind::String);
939        assert!(tokens[0].flags.has(Flags::UNTERMINATED));
940        assert_eq!(tokens[0].text("$$abc"), "$$abc");
941    }
942
943    #[test]
944    fn a_parameter_is_two_tokens() {
945        // There is no parameter token kind. The grammar spells `$` followed by a number or a
946        // name, so the tokenizer never has to decide which one it is looking at.
947        assert_eq!(scan("$1"), [(Kind::Operator, "$"), (Kind::Number, "1")]);
948        assert_eq!(scan("$banana"), [(Kind::Operator, "$"), (Kind::Identifier, "banana")]);
949        assert_eq!(scan("?"), [(Kind::Operator, "?")]);
950        // A lone dollar has nothing after it to decide with and falls out as an identifier.
951        assert_eq!(scan("$"), [(Kind::Identifier, "$")]);
952    }
953
954    #[test]
955    fn a_semicolon_is_its_own_kind() {
956        assert_eq!(
957            scan("SELECT 1; SELECT 2"),
958            [
959                (Kind::Keyword, "SELECT"),
960                (Kind::Number, "1"),
961                (Kind::Terminator, ";"),
962                (Kind::Keyword, "SELECT"),
963                (Kind::Number, "2"),
964            ]
965        );
966        assert_eq!(scan(";"), [(Kind::Terminator, ";")]);
967    }
968
969    #[test]
970    fn the_four_errors_are_the_four_upstream_throws() {
971        assert_eq!(message("SELECT /* x"), "unterminated /* comment at or near \"/* x\"");
972        assert_eq!(message("SELECT 'x"), "unterminated string literal");
973        assert_eq!(message("SELECT \"x"), "unterminated quoted identifier");
974        assert_eq!(message("SELECT \"\""), "zero-length delimited identifier");
975        // And an error points at where the trouble started, not at the end of the query.
976        assert_eq!(tokenize("SELECT 'x").expect_err("fails").span().map(|s| s.start), Some(7));
977    }
978
979    #[test]
980    fn every_span_lands_where_the_text_is() {
981        let query = "SELECT a, /*c*/ 'b' || $$d$$ FROM t;";
982        for token in tokenize(query).expect("tokenizes") {
983            assert!(token.end as usize <= query.len());
984            assert!(token.start <= token.end);
985            if token.kind != Kind::EndOfInput {
986                assert!(!token.text(query).is_empty());
987            }
988        }
989    }
990
991    #[test]
992    fn a_real_query_comes_out_the_way_it_reads() {
993        assert_eq!(
994            scan("SELECT count(*) FROM t WHERE x > 5 AND y::VARCHAR = 'a';"),
995            [
996                (Kind::Keyword, "SELECT"),
997                (Kind::Identifier, "count"),
998                (Kind::Operator, "("),
999                (Kind::Operator, "*"),
1000                (Kind::Operator, ")"),
1001                (Kind::Keyword, "FROM"),
1002                (Kind::Identifier, "t"),
1003                (Kind::Keyword, "WHERE"),
1004                (Kind::Identifier, "x"),
1005                (Kind::Operator, ">"),
1006                (Kind::Number, "5"),
1007                (Kind::Keyword, "AND"),
1008                (Kind::Identifier, "y"),
1009                (Kind::Operator, "::"),
1010                (Kind::Keyword, "VARCHAR"),
1011                (Kind::Operator, "="),
1012                (Kind::String, "'a'"),
1013                (Kind::Terminator, ";"),
1014            ]
1015        );
1016    }
1017}