Skip to content

Commit cc080fd

Browse files
committed
tokenizer: reuse the word buffer
1 parent 1ea2488 commit cc080fd

3 files changed

Lines changed: 118 additions & 5 deletions

File tree

‎sqlparser_bench/benches/sqlparser_bench.rs‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@
1616
// under the License.
1717

1818
use criterion::{criterion_group, criterion_main, Criterion};
19-
use sqlparser::dialect::{GenericDialect, PostgreSqlDialect, SQLiteDialect};
19+
use sqlparser::dialect::{GenericDialect, MySqlDialect, PostgreSqlDialect, SQLiteDialect};
2020
use sqlparser::keywords::Keyword;
2121
use sqlparser::parser::Parser;
22-
use sqlparser::tokenizer::{Span, Word};
22+
use sqlparser::tokenizer::{Span, Tokenizer, Word};
2323

2424
fn basic_queries(c: &mut Criterion) {
2525
let mut group = c.benchmark_group("sqlparser-rs parsing benchmark");
@@ -297,9 +297,29 @@ fn parse_function_arg_call_chain(c: &mut Criterion) {
297297
group.finish();
298298
}
299299

300+
fn tokenize_identifiers(c: &mut Criterion) {
301+
let mut group = c.benchmark_group("tokenize_identifiers");
302+
let dialect = MySqlDialect {};
303+
for len in [8, 128] {
304+
let word = "a".repeat(len);
305+
let sql = format!("SELECT {word}, {word} FROM {word} WHERE {word} = 42");
306+
group.bench_function(format!("length_{len}"), |b| {
307+
b.iter(|| {
308+
std::hint::black_box(
309+
Tokenizer::new(&dialect, std::hint::black_box(&sql))
310+
.tokenize_with_location()
311+
.unwrap(),
312+
);
313+
});
314+
});
315+
}
316+
group.finish();
317+
}
318+
300319
criterion_group!(
301320
benches,
302321
basic_queries,
322+
tokenize_identifiers,
303323
word_to_ident,
304324
parse_many_identifiers,
305325
parse_compound_chain,

‎src/tokenizer.rs‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2090,9 +2090,13 @@ impl<'a> Tokenizer<'a> {
20902090
/// Tokenize an identifier or keyword, after the first char is already consumed.
20912091
fn tokenize_word(&self, first_chars: impl Into<String>, chars: &mut State) -> String {
20922092
let mut s = first_chars.into();
2093-
s.push_str(&peeking_take_while(chars, |ch| {
2094-
self.dialect.is_identifier_part(ch)
2095-
}));
2093+
while let Some(&ch) = chars.peek() {
2094+
if !self.dialect.is_identifier_part(ch) {
2095+
break;
2096+
}
2097+
chars.next();
2098+
s.push(ch);
2099+
}
20962100
s
20972101
}
20982102

‎tests/sqlparser_common.rs‎

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20856,3 +20856,92 @@ fn parse_window_spec_keyword_base_window_name() {
2085620856
"SELECT count(*) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM t",
2085720857
);
2085820858
}
20859+
20860+
#[test]
20861+
fn tokenize_identifier_prefixes_and_unicode_spans() {
20862+
let suffixes = [
20863+
String::new(),
20864+
"_name".to_owned(),
20865+
"alpha".to_owned(),
20866+
"é".to_owned(),
20867+
"漢字".to_owned(),
20868+
"λ".to_owned(),
20869+
"\u{0301}".to_owned(),
20870+
"a".repeat(128),
20871+
];
20872+
for dialect in all_dialects().dialects {
20873+
for prefix in [
20874+
"a", "b", "B", "n", "N", "nq", "NQ", "nQ", "Nq", "q", "Q", "r", "R", "x", "X", "e",
20875+
"E", "u", "U", "é", "漢", "λ",
20876+
] {
20877+
for suffix in &suffixes {
20878+
let word = format!("{prefix}{suffix}");
20879+
let mut chars = word.chars();
20880+
if !dialect.is_identifier_start(chars.next().unwrap())
20881+
|| !chars.all(|ch| dialect.is_identifier_part(ch))
20882+
{
20883+
continue;
20884+
}
20885+
let sql = format!("\n{word}\t/*tail*/");
20886+
let tokens = Tokenizer::new(&*dialect, &sql)
20887+
.tokenize_with_location()
20888+
.unwrap();
20889+
assert_eq!(
20890+
tokens.iter().map(|t| t.token.clone()).collect::<Vec<_>>(),
20891+
vec![
20892+
Token::Whitespace(Whitespace::Newline),
20893+
Token::make_word(&word, None),
20894+
Token::Whitespace(Whitespace::Tab),
20895+
Token::Whitespace(Whitespace::MultiLineComment("tail".to_owned())),
20896+
],
20897+
"{dialect:?}: {sql:?}"
20898+
);
20899+
assert_eq!(
20900+
tokens[1].span,
20901+
Span::new(
20902+
Location::new(2, 1),
20903+
Location::new(2, word.chars().count() as u64 + 1)
20904+
),
20905+
"{dialect:?}: {sql:?}"
20906+
);
20907+
}
20908+
}
20909+
}
20910+
}
20911+
20912+
#[test]
20913+
fn tokenize_identifier_at_eof_and_before_error() {
20914+
for dialect in all_dialects().dialects {
20915+
let word = "nQalpha";
20916+
let tokens = Tokenizer::new(&*dialect, word)
20917+
.tokenize_with_location()
20918+
.unwrap();
20919+
assert_eq!(tokens.len(), 1, "{dialect:?}");
20920+
assert_eq!(tokens[0].token, Token::make_word(word, None));
20921+
assert_eq!(
20922+
tokens[0].span,
20923+
Span::new(Location::new(1, 1), Location::new(1, 8))
20924+
);
20925+
20926+
let mut buf = tokens.clone();
20927+
let error = Tokenizer::new(&*dialect, "nQalpha '")
20928+
.tokenize_with_location_into_buf(&mut buf)
20929+
.unwrap_err();
20930+
assert_eq!(error.message, "Unterminated string literal", "{dialect:?}");
20931+
// Quote error positions differ by dialect.
20932+
let quote_error = Tokenizer::new(&*dialect, "'").tokenize().unwrap_err();
20933+
assert_eq!(
20934+
error.location,
20935+
Location::new(1, quote_error.location.column + word.len() as u64 + 1),
20936+
"{dialect:?}"
20937+
);
20938+
assert_eq!(buf.len(), 3, "{dialect:?}");
20939+
assert_eq!(buf[0], tokens[0]);
20940+
assert_eq!(buf[1], tokens[0]);
20941+
assert_eq!(buf[2].token, Token::Whitespace(Whitespace::Space));
20942+
assert_eq!(
20943+
buf[2].span,
20944+
Span::new(Location::new(1, 8), Location::new(1, 9))
20945+
);
20946+
}
20947+
}

0 commit comments

Comments
 (0)