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
588/// An identifier as DuckDB's deparser writes it, quoted when it has to be.
589///
590/// Two grounds for quoting. One is that the word is a keyword in a class, so a column called `name`
591/// comes back as `"name"` while one called `alias` comes back bare, since `alias` is spelled by a
592/// grammar rule and is in no class. The other is that the text is not one word the tokenizer above
593/// would read back, so `my col`, `9x` and `é` keep their quotes.
594///
595/// Case is not a ground. `UserID` comes back exactly like that even though reading it again folds
596/// it, which is the whole reason ClickBench's column names agree between the two engines.
597///
598/// Here rather than in the binder because two callers want the same rule: a generated column name
599/// and the `sql` column of `duckdb_tables()`, which are both a deparser writing an identifier back
600/// out for somebody to read.
601#[must_use]
602pub fn quoted(text: &str) -> String {
603    let mut bytes = text.bytes();
604    let plain = matches!(bytes.next(), Some(byte) if byte.is_ascii_alphabetic() || byte == b'_')
605        && bytes.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_');
606    if plain && classes(lookup(text)) == 0 {
607        return text.to_string();
608    }
609    format!("\"{}\"", text.replace('"', "\"\""))
610}
611
612/// A dotted name split into its parts, with the quoting taken off each one.
613///
614/// The inverse of [`quoted`] over a whole name rather than over one identifier, and the reason it
615/// is here is that the two rules have to be the same rule. `pragma_table_info('main.t')` is a table
616/// name that arrived as a string rather than as a name the parser read, so somebody has to decide
617/// where the parts of it end, and the only right answer is wherever the tokenizer would have said
618/// they end. A table created as `"a.b"` is one part and a table written `a.b` is two.
619///
620/// Quoting is per part. `"a.b".c` is `a.b` and `c`, a doubled quote inside a quoted part is one
621/// quote, and a part with no quotes keeps whatever case it was written in, because nothing in this
622/// crate folds a name. An unterminated quote takes the rest of the text, which is the same thing
623/// the tokenizer does with one.
624///
625/// The empty string is one empty part rather than no parts at all. That is what makes an empty name
626/// a name the catalog can say it does not have, and upstream is worth being plain about here: the
627/// pinned binary answers `Invalid Error: cannot create std::vector larger than max_size()` for
628/// `pragma_table_info('')`, which is tamnd/duckdb#5, while `pragma_table_info('.')` on the same
629/// build is an ordinary catalog error about a table with no name. The two are the same name, so
630/// this returns the same parts for both and the catalog gives the same answer to each.
631#[must_use]
632pub fn identifier_parts(text: &str) -> Vec<String> {
633    let mut parts = Vec::new();
634    let mut part = String::new();
635    let mut rest = text.chars().peekable();
636    while let Some(c) = rest.next() {
637        match c {
638            '.' => parts.push(std::mem::take(&mut part)),
639            '"' => {
640                while let Some(inside) = rest.next() {
641                    if inside != '"' {
642                        part.push(inside);
643                        continue;
644                    }
645                    // A doubled quote is one quote and the part carries on, and a single one ends
646                    // the part, which is the tokenizer's rule for a quoted identifier.
647                    if rest.peek() == Some(&'"') {
648                        rest.next();
649                        part.push('"');
650                    } else {
651                        break;
652                    }
653                }
654            }
655            _ => part.push(c),
656        }
657    }
658    parts.push(part);
659    parts
660}
661
662const fn is_space(c: u8) -> bool {
663    matches!(c, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r')
664}
665
666/// The dozen characters that are always their own token and never join a run.
667///
668/// `-` and `#` being in here is the surprise. It is why `SELECT 1 =- 1` is `1 = -1` and why `--`
669/// can be a comment without ever having to be told it is not an operator.
670const fn is_single_byte_operator(c: u8) -> bool {
671    matches!(c, b'(' | b')' | b'{' | b'}' | b'[' | b']' | b',' | b'?' | b'$' | b'-' | b'#')
672}
673
674/// PostgreSQL's operator character set, which is every ASCII punctuation character except `_`.
675const fn is_operator_char(c: u8) -> bool {
676    if c == b'_' {
677        return false;
678    }
679    matches!(c, b'!'..=b'/' | b':'..=b'@' | b'['..=b'`' | b'{'..=b'~')
680}
681
682/// Whether `c` can continue an operator run.
683///
684/// The single byte operators are excluded, and so are the five characters that always end one:
685/// `'`, `-`, `;`, `"` and `.`. `-` is in both lists and that is not redundant, since the first
686/// list is also consulted where the second is not.
687const fn is_operator_char_in_run(c: u8) -> bool {
688    if is_single_byte_operator(c) || is_control_flow(c) {
689        return false;
690    }
691    is_operator_char(c)
692}
693
694const fn is_control_flow(c: u8) -> bool {
695    matches!(c, b'\'' | b'-' | b';' | b'"' | b'.')
696}
697
698/// Whether `c` can continue a bare word.
699///
700/// Anything that is not punctuation, whitespace or a delimiter, which by the arithmetic above
701/// includes every byte at or above 0x80. So an identifier may be any UTF-8 the user likes, and
702/// the tokenizer never has to decode it to find that out.
703const fn is_word_char(c: u8) -> bool {
704    if is_single_byte_operator(c) || is_operator_char(c) || is_space(c) || is_control_flow(c) {
705        return false;
706    }
707    true
708}
709
710/// A digit or a dot, which is both what starts a number and what can appear anywhere in one.
711///
712/// A dot being unconditional here is why `SELECT 1.2.3` is a single number token rather than
713/// three things. What it means is somebody else's problem, which is exactly how upstream has it.
714const fn is_initial_number(c: u8) -> bool {
715    c.is_ascii_digit() || c == b'.'
716}
717
718const fn is_scientific(c: u8) -> bool {
719    c == b'e' || c == b'E'
720}
721
722const fn is_string_prefix(c: u8) -> bool {
723    matches!(c, b'N' | b'n' | b'X' | b'x' | b'E' | b'e' | b'B' | b'b')
724}
725
726/// A-Z, a-z, 0-9, `_`, and anything at or above 0x80. Digits are only legal after the first byte
727/// and the caller checks that.
728const fn is_dollar_tag_char(c: u8) -> bool {
729    c.is_ascii_alphanumeric() || c == b'_' || c >= 0x80
730}
731
732/// The six sequences checked before the maximal run, longest first.
733///
734/// `->` is here because `-` is a single byte operator and would otherwise never join anything,
735/// which would leave the JSON arrow unspellable.
736fn special_operator(bytes: &[u8], at: usize) -> Option<usize> {
737    if bytes[at..].starts_with(b"->>") {
738        return Some(3);
739    }
740    for candidate in [b"::".as_slice(), b":=", b"->", b"**", b"//"] {
741        if bytes[at..].starts_with(candidate) {
742            return Some(2);
743        }
744    }
745    None
746}
747
748#[cfg(test)]
749mod tests {
750    use super::{classes, identifier_parts, lookup, quoted, tokenize};
751    use crate::generated::keywords::{RESERVED, UNRESERVED};
752    use crate::token::{Flags, Kind, NOT_A_KEYWORD, Token};
753
754    #[test]
755    fn a_deparsed_identifier_is_quoted_on_a_keyword_or_a_word_that_will_not_read_back() {
756        // A keyword in a class, which is what makes `min(name)` come back as `min("name")`.
757        assert_eq!(quoted("name"), "\"name\"");
758        // A soft word, in the table with no class, which does not block an identifier and so stays
759        // bare. This is the pair that makes the rule worth asking the table about rather than
760        // guessing from the keyword list.
761        assert_eq!(quoted("alias"), "alias");
762        assert_eq!(quoted("x"), "x");
763        assert_eq!(quoted("_x9"), "_x9");
764        assert_eq!(quoted("my col"), "\"my col\"");
765        assert_eq!(quoted("9x"), "\"9x\"");
766        assert_eq!(quoted("e\u{301}"), "\"e\u{301}\"");
767        // Case is not a ground, which is the whole reason ClickBench's column names agree with
768        // DuckDB's even though reading one back folds it.
769        assert_eq!(quoted("UserID"), "UserID");
770        // A quote inside doubles, so the result reads back as the name that went in.
771        assert_eq!(quoted("a\"b"), "\"a\"\"b\"");
772    }
773
774    /// Every token but the sentinel, as the pair worth asserting on.
775    fn scan(query: &str) -> Vec<(Kind, &str)> {
776        let tokens = tokenize(query).expect("tokenizes");
777        assert_eq!(tokens.last().map(|t| t.kind), Some(Kind::EndOfInput));
778        assert_eq!(tokens.iter().filter(|t| t.kind == Kind::EndOfInput).count(), 1);
779        tokens[..tokens.len() - 1].iter().map(|t| (t.kind, t.text(query))).collect()
780    }
781
782    fn texts(query: &str) -> Vec<&str> {
783        scan(query).into_iter().map(|(_, text)| text).collect()
784    }
785
786    fn all(query: &str) -> Vec<Token> {
787        tokenize(query).expect("tokenizes")
788    }
789
790    fn message(query: &str) -> String {
791        tokenize(query).expect_err("fails").message().to_string()
792    }
793
794    #[test]
795    fn the_empty_query_is_one_sentinel() {
796        let tokens = tokenize("").expect("tokenizes");
797        assert_eq!(tokens.len(), 1);
798        assert_eq!(tokens[0].kind, Kind::EndOfInput);
799        assert_eq!(tokens[0].span(), rudb_common::Span::new(0, 0));
800        assert!(scan("   \t\n  ").is_empty());
801    }
802
803    #[test]
804    fn a_word_in_a_class_is_a_keyword_and_one_in_none_is_not() {
805        assert_eq!(scan("SELECT"), [(Kind::Keyword, "SELECT")]);
806        assert_eq!(scan("banana"), [(Kind::Identifier, "banana")]);
807    }
808
809    #[test]
810    fn case_is_matched_but_not_folded() {
811        // DuckDB is case insensitive and case preserving, and unlike PostgreSQL that holds for
812        // quoted identifiers too. So the keyword is recognized whatever its case and the bytes
813        // come back exactly as written. Folding here would be the wrong answer twice over.
814        for spelling in ["select", "SELECT", "SeLeCt"] {
815            let tokens = all(spelling);
816            assert_eq!(tokens[0].kind, Kind::Keyword);
817            assert_eq!(tokens[0].text(spelling), spelling);
818            assert_eq!(tokens[0].keyword, lookup("select"));
819        }
820        assert_eq!(scan(r#""Foo""#), [(Kind::QuotedIdentifier, r#""Foo""#)]);
821    }
822
823    #[test]
824    fn a_soft_word_keeps_its_index_and_stays_an_identifier() {
825        // ASCENDING is spelled by a rule and is in none of the five lists. If it came back as a
826        // keyword then `SELECT ascending FROM t` would stop working, and if it came back without
827        // an index then `ORDER BY x ASCENDING` could not be filtered on the literal.
828        let tokens = all("ascending");
829        assert_eq!(tokens[0].kind, Kind::Identifier);
830        assert_ne!(tokens[0].keyword, NOT_A_KEYWORD);
831        assert_eq!(classes(tokens[0].keyword), 0);
832
833        let tokens = all("banana");
834        assert_eq!(tokens[0].keyword, NOT_A_KEYWORD);
835        assert_eq!(classes(tokens[0].keyword), 0);
836    }
837
838    #[test]
839    fn the_classes_come_back_off_the_index() {
840        assert_eq!(classes(lookup("select")) & RESERVED, RESERVED);
841        assert_eq!(classes(lookup("abort")) & UNRESERVED, UNRESERVED);
842        assert_eq!(lookup("supercalifragilistic"), NOT_A_KEYWORD);
843        assert_eq!(lookup(""), NOT_A_KEYWORD);
844    }
845
846    #[test]
847    fn a_quoted_identifier_is_never_a_keyword() {
848        let tokens = all(r#""select""#);
849        assert_eq!(tokens[0].kind, Kind::QuotedIdentifier);
850        assert_eq!(tokens[0].keyword, NOT_A_KEYWORD);
851        assert_eq!(scan(r#""a""b""#), [(Kind::QuotedIdentifier, r#""a""b""#)]);
852    }
853
854    #[test]
855    fn a_dollar_is_an_identifier_character_after_the_first_byte() {
856        assert_eq!(scan("a$b"), [(Kind::Identifier, "a$b")]);
857    }
858
859    #[test]
860    fn any_byte_above_ascii_is_an_identifier_character() {
861        // Nothing decodes UTF-8 here. The arithmetic in `is_word_char` says every byte at or
862        // above 0x80 continues a word, which is how an identifier in any script gets through
863        // without the tokenizer knowing what script it is.
864        assert_eq!(scan("SELECT café"), [(Kind::Keyword, "SELECT"), (Kind::Identifier, "café")]);
865    }
866
867    #[test]
868    fn a_number_swallows_more_than_a_number() {
869        // Every one of these is a single NUMBER token upstream and the parser deals with what it
870        // means. `1.2.3` in particular is not three things.
871        assert_eq!(scan("1.2.3"), [(Kind::Number, "1.2.3")]);
872        assert_eq!(scan("1_000"), [(Kind::Number, "1_000")]);
873        assert_eq!(scan("1e5"), [(Kind::Number, "1e5")]);
874        assert_eq!(scan(".1e5"), [(Kind::Number, ".1e5")]);
875        assert_eq!(scan("1e-5"), [(Kind::Number, "1e-5")]);
876        assert_eq!(scan("1.e5"), [(Kind::Number, "1.e5")]);
877    }
878
879    #[test]
880    fn a_trailing_e_stays_on_the_number() {
881        // `SELECT 1e` is one NUMBER token "1e" and not `1` aliased `e`, because the tokenizer
882        // takes the `e` and only the parser is in a position to object.
883        assert_eq!(scan("SELECT 1e"), [(Kind::Keyword, "SELECT"), (Kind::Number, "1e")]);
884        assert_eq!(scan("SELECT 1e+"), [(Kind::Keyword, "SELECT"), (Kind::Number, "1e+")]);
885    }
886
887    #[test]
888    fn what_the_number_cannot_use_it_gives_back() {
889        // The backtrack at the end of NUMERIC only keeps digits and dots, so `1e+ 1` unwinds the
890        // whole exponent it started and the `e` comes back as a name.
891        assert_eq!(
892            scan("SELECT 1e+ 1"),
893            [
894                (Kind::Keyword, "SELECT"),
895                (Kind::Number, "1"),
896                (Kind::Identifier, "e"),
897                (Kind::Operator, "+"),
898                (Kind::Number, "1"),
899            ]
900        );
901        assert_eq!(scan("1_"), [(Kind::Number, "1"), (Kind::Identifier, "_")]);
902        assert_eq!(scan("1__0"), [(Kind::Number, "1"), (Kind::Identifier, "__0")]);
903        // There is no hex literal. `0x1F` is zero and then a name, which is worth knowing before
904        // somebody reports it as a bug in the parser.
905        assert_eq!(scan("0x1F"), [(Kind::Number, "0"), (Kind::Identifier, "x1F")]);
906        assert_eq!(scan(".e100"), [(Kind::Number, "."), (Kind::Identifier, "e100")]);
907    }
908
909    #[test]
910    fn a_minus_never_joins_an_operator_run() {
911        // `-` is a single byte operator, which is what makes `SELECT 1 =- 1` a subtraction of a
912        // negative one rather than a call to an operator named `=-`.
913        assert_eq!(texts("1-1"), ["1", "-", "1"]);
914        assert_eq!(texts("SELECT 1 =- 1"), ["SELECT", "1", "=", "-", "1"]);
915        assert_eq!(texts("(a,b)"), ["(", "a", ",", "b", ")"]);
916    }
917
918    #[test]
919    fn the_postgres_plus_rule_decides_where_a_run_ends() {
920        // An operator cannot end in `+` unless it contains one of ~ ! @ # % ^ & | ` ?. This is
921        // the rule that lets a user defined operator be told apart from an operator and a sign.
922        assert_eq!(texts("SELECT 1 =+ 1"), ["SELECT", "1", "=", "+", "1"]);
923        assert_eq!(texts("SELECT 1 !=+ 1"), ["SELECT", "1", "!=+", "1"]);
924        assert_eq!(texts("SELECT 1 =++ 1"), ["SELECT", "1", "=", "+", "+", "1"]);
925        assert_eq!(texts("SELECT 1 ++ 1"), ["SELECT", "1", "+", "+", "1"]);
926    }
927
928    #[test]
929    fn the_special_operators_are_checked_before_the_run() {
930        assert_eq!(texts("a->>'b'"), ["a", "->>", "'b'"]);
931        assert_eq!(texts("a->'b'"), ["a", "->", "'b'"]);
932        assert_eq!(texts("a::b"), ["a", "::", "b"]);
933        assert_eq!(texts("a//b"), ["a", "//", "b"]);
934        assert_eq!(texts("2**3"), ["2", "**", "3"]);
935        // But only when what follows is not itself an operator character, in which case the
936        // maximal run wins and it is one token.
937        assert_eq!(texts("a::=b"), ["a", "::=", "b"]);
938    }
939
940    #[test]
941    fn a_block_comment_can_end_an_operator_run() {
942        assert_eq!(texts("1+/*c*/2"), ["1", "+", "2"]);
943    }
944
945    #[test]
946    fn a_comment_is_not_a_token_but_a_block_one_leaves_a_mark() {
947        assert_eq!(texts("SELECT --x\n1"), ["SELECT", "1"]);
948        assert_eq!(texts("SELECT /*x*/ 1"), ["SELECT", "1"]);
949        assert_eq!(texts("SELECT --x"), ["SELECT"]);
950
951        let tokens = all("SELECT /*x*/ 1");
952        assert!(tokens[1].flags.has(Flags::BLOCK_COMMENT));
953        assert!(!tokens[1].flags.has(Flags::NEWLINE));
954
955        // A line comment is not a block comment, and the newline that ends it still counts.
956        let tokens = all("SELECT --x\n1");
957        assert!(!tokens[1].flags.has(Flags::BLOCK_COMMENT));
958        assert!(tokens[1].flags.has(Flags::NEWLINE));
959    }
960
961    #[test]
962    fn a_hint_is_a_comment_that_can_be_read_back() {
963        assert_eq!(
964            super::hints("SELECT /*+ hash.table(unchained) */ 1").unwrap(),
965            [" hash.table(unchained) "]
966        );
967        // Still a comment, so the tokens are what they would have been without it.
968        assert_eq!(texts("SELECT /*+ hash.table(unchained) */ 1"), ["SELECT", "1"]);
969
970        // Two of them, in the order they were written, from anywhere in the statement.
971        assert_eq!(super::hints("SELECT /*+ a(b) */ 1 /*+ c(d) */").unwrap(), [" a(b) ", " c(d) "]);
972
973        // A comment without the plus is not a hint, and neither is one inside a string literal,
974        // which is the case a scanner that did not know about literals would get wrong.
975        assert!(super::hints("SELECT /* hash.table(unchained) */ 1").unwrap().is_empty());
976        assert!(super::hints("SELECT '/*+ hash.table(unchained) */'").unwrap().is_empty());
977        assert!(super::hints("SELECT 1").unwrap().is_empty());
978    }
979
980    #[test]
981    fn the_first_token_is_preceded_by_nothing() {
982        let tokens = all("\n/*x*/ SELECT");
983        assert_eq!(tokens[0].flags, Flags::default());
984    }
985
986    #[test]
987    fn block_comments_nest() {
988        // The reason this matters is commenting out a block that already contains a comment. In
989        // PostgreSQL it works, in most SQL dialects it does not, and DuckDB followed PostgreSQL.
990        assert_eq!(texts("SELECT /* a /* b */ c */ 1"), ["SELECT", "1"]);
991        assert_eq!(
992            message("SELECT /* a /* b */ 1"),
993            "unterminated /* comment at or near \"/* a /* b */ 1\""
994        );
995    }
996
997    #[test]
998    fn a_string_keeps_its_quotes_and_its_escapes() {
999        assert_eq!(scan("'it''s'"), [(Kind::String, "'it''s'")]);
1000        assert_eq!(scan("''"), [(Kind::String, "''")]);
1001        assert_eq!(texts("'a' 'b'"), ["'a'", "'b'"]);
1002    }
1003
1004    #[test]
1005    fn only_the_e_prefix_changes_how_a_string_is_read() {
1006        // In an E string a backslash escapes the next byte, so the doubled quote at the end is
1007        // one escaped quote and then the close. Without the prefix the backslash is an ordinary
1008        // character, the doubled quote is an escape, and the literal runs off the end.
1009        assert_eq!(scan(r"E'\''"), [(Kind::String, r"E'\''")]);
1010        assert_eq!(message(r"'\''"), "unterminated string literal");
1011        for prefix in ["X", "x", "B", "b", "N", "n", "E", "e"] {
1012            let query = format!("{prefix}'a'");
1013            assert_eq!(tokenize(&query).expect("tokenizes")[0].kind, Kind::String);
1014        }
1015        // The quote has to be the very next byte, otherwise it is a name and then a string.
1016        assert_eq!(texts("x 'a'"), ["x", "'a'"]);
1017    }
1018
1019    #[test]
1020    fn a_dollar_quoted_string_is_one_token_and_its_tag_has_to_match() {
1021        assert_eq!(scan("$$abc$$"), [(Kind::String, "$$abc$$")]);
1022        assert_eq!(scan("$tag$abc$tag$"), [(Kind::String, "$tag$abc$tag$")]);
1023        assert_eq!(scan("$tag$a$other$b$tag$"), [(Kind::String, "$tag$a$other$b$tag$")]);
1024        assert_eq!(scan("$$it's fine$$"), [(Kind::String, "$$it's fine$$")]);
1025    }
1026
1027    #[test]
1028    fn an_unterminated_dollar_quote_is_a_token_and_not_an_error() {
1029        // The other three unterminated forms throw. This one does not, which is upstream's
1030        // choice and not ours, and the flag is how the difference reaches the matcher.
1031        let tokens = all("$$abc");
1032        assert_eq!(tokens[0].kind, Kind::String);
1033        assert!(tokens[0].flags.has(Flags::UNTERMINATED));
1034        assert_eq!(tokens[0].text("$$abc"), "$$abc");
1035    }
1036
1037    #[test]
1038    fn a_parameter_is_two_tokens() {
1039        // There is no parameter token kind. The grammar spells `$` followed by a number or a
1040        // name, so the tokenizer never has to decide which one it is looking at.
1041        assert_eq!(scan("$1"), [(Kind::Operator, "$"), (Kind::Number, "1")]);
1042        assert_eq!(scan("$banana"), [(Kind::Operator, "$"), (Kind::Identifier, "banana")]);
1043        assert_eq!(scan("?"), [(Kind::Operator, "?")]);
1044        // A lone dollar has nothing after it to decide with and falls out as an identifier.
1045        assert_eq!(scan("$"), [(Kind::Identifier, "$")]);
1046    }
1047
1048    #[test]
1049    fn a_semicolon_is_its_own_kind() {
1050        assert_eq!(
1051            scan("SELECT 1; SELECT 2"),
1052            [
1053                (Kind::Keyword, "SELECT"),
1054                (Kind::Number, "1"),
1055                (Kind::Terminator, ";"),
1056                (Kind::Keyword, "SELECT"),
1057                (Kind::Number, "2"),
1058            ]
1059        );
1060        assert_eq!(scan(";"), [(Kind::Terminator, ";")]);
1061    }
1062
1063    #[test]
1064    fn the_four_errors_are_the_four_upstream_throws() {
1065        assert_eq!(message("SELECT /* x"), "unterminated /* comment at or near \"/* x\"");
1066        assert_eq!(message("SELECT 'x"), "unterminated string literal");
1067        assert_eq!(message("SELECT \"x"), "unterminated quoted identifier");
1068        assert_eq!(message("SELECT \"\""), "zero-length delimited identifier");
1069        // And an error points at where the trouble started, not at the end of the query.
1070        assert_eq!(tokenize("SELECT 'x").expect_err("fails").span().map(|s| s.start), Some(7));
1071    }
1072
1073    #[test]
1074    fn every_span_lands_where_the_text_is() {
1075        let query = "SELECT a, /*c*/ 'b' || $$d$$ FROM t;";
1076        for token in tokenize(query).expect("tokenizes") {
1077            assert!(token.end as usize <= query.len());
1078            assert!(token.start <= token.end);
1079            if token.kind != Kind::EndOfInput {
1080                assert!(!token.text(query).is_empty());
1081            }
1082        }
1083    }
1084
1085    #[test]
1086    fn a_real_query_comes_out_the_way_it_reads() {
1087        assert_eq!(
1088            scan("SELECT count(*) FROM t WHERE x > 5 AND y::VARCHAR = 'a';"),
1089            [
1090                (Kind::Keyword, "SELECT"),
1091                (Kind::Identifier, "count"),
1092                (Kind::Operator, "("),
1093                (Kind::Operator, "*"),
1094                (Kind::Operator, ")"),
1095                (Kind::Keyword, "FROM"),
1096                (Kind::Identifier, "t"),
1097                (Kind::Keyword, "WHERE"),
1098                (Kind::Identifier, "x"),
1099                (Kind::Operator, ">"),
1100                (Kind::Number, "5"),
1101                (Kind::Keyword, "AND"),
1102                (Kind::Identifier, "y"),
1103                (Kind::Operator, "::"),
1104                (Kind::Keyword, "VARCHAR"),
1105                (Kind::Operator, "="),
1106                (Kind::String, "'a'"),
1107                (Kind::Terminator, ";"),
1108            ]
1109        );
1110    }
1111
1112    /// The parts of a name in a string, which is how a pragma is handed a table to describe.
1113    #[test]
1114    fn a_dotted_name_splits_where_the_tokenizer_would_split_it() {
1115        assert_eq!(identifier_parts("t"), ["t"]);
1116        assert_eq!(identifier_parts("main.t"), ["main", "t"]);
1117        assert_eq!(identifier_parts("memory.main.t"), ["memory", "main", "t"]);
1118        // Case survives, because nothing in this crate folds a name.
1119        assert_eq!(identifier_parts("Memory.Main.MyTable"), ["Memory", "Main", "MyTable"]);
1120    }
1121
1122    /// A quoted part keeps whatever is inside it, dots included, which is the whole reason this
1123    /// cannot be a split on the character.
1124    #[test]
1125    fn a_quoted_part_holds_the_dots_that_are_inside_it() {
1126        assert_eq!(identifier_parts("\"a.b\""), ["a.b"]);
1127        assert_eq!(identifier_parts("\"a.b\".c"), ["a.b", "c"]);
1128        assert_eq!(identifier_parts("\"a\"\"b\""), ["a\"b"]);
1129        assert_eq!(identifier_parts("main.\"My Table\""), ["main", "My Table"]);
1130        // An unterminated quote takes the rest, the way the tokenizer reads one.
1131        assert_eq!(identifier_parts("\"a.b"), ["a.b"]);
1132    }
1133
1134    /// An empty name is a name, and a name with an empty part in it is as well.
1135    #[test]
1136    fn an_empty_name_is_one_empty_part_rather_than_nothing() {
1137        assert_eq!(identifier_parts(""), [""]);
1138        assert_eq!(identifier_parts("."), ["", ""]);
1139        assert_eq!(identifier_parts("a."), ["a", ""]);
1140        assert_eq!(identifier_parts("memory..t"), ["memory", "", "t"]);
1141    }
1142}