Skip to content

Commit ee873bd

Browse files
committed
Unify numeric separator validation
1 parent d4e5aa9 commit ee873bd

2 files changed

Lines changed: 54 additions & 63 deletions

File tree

src/tokenizer.rs

Lines changed: 51 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1360,36 +1360,12 @@ impl<'a> Tokenizer<'a> {
13601360
);
13611361
}
13621362

1363-
// Some dialects support underscore as number separator
1364-
// There can only be one at a time and it must be followed by another digit
1365-
let is_number_separator = |ch: char, next_char: Option<char>| {
1366-
self.dialect.supports_numeric_literal_underscores()
1367-
&& ch == '_'
1368-
&& next_char.is_some_and(|next_ch| next_ch.is_ascii_hexdigit())
1369-
};
1370-
1371-
let mut s = peeking_next_take_while(chars, |ch, next_ch| {
1372-
ch.is_ascii_digit() || is_number_separator(ch, next_ch)
1373-
});
1374-
1375-
if self.dialect.supports_numeric_literal_underscores()
1376-
&& chars.peek() == Some(&'_')
1377-
{
1378-
return self.tokenizer_error(chars.location(), "Unexpected character '_'");
1379-
}
1363+
let mut s = self.tokenize_number_part(chars, |ch| ch.is_ascii_digit())?;
13801364

13811365
// match binary literal that starts with 0x
13821366
if s == "0" && chars.peek() == Some(&'x') {
13831367
chars.next();
1384-
let s2 = peeking_next_take_while(chars, |ch, next_ch| {
1385-
ch.is_ascii_hexdigit() || is_number_separator(ch, next_ch)
1386-
});
1387-
if self.dialect.supports_numeric_literal_underscores()
1388-
&& chars.peek() == Some(&'_')
1389-
{
1390-
return self
1391-
.tokenizer_error(chars.location(), "Unexpected character '_'");
1392-
}
1368+
let s2 = self.tokenize_number_part(chars, |ch| ch.is_ascii_hexdigit())?;
13931369
return Ok(Some(Token::HexStringLiteral(s2)));
13941370
}
13951371

@@ -1410,23 +1386,8 @@ impl<'a> Tokenizer<'a> {
14101386
}
14111387
}
14121388

1413-
if s != "."
1414-
&& self.dialect.supports_numeric_literal_underscores()
1415-
&& chars.peek() == Some(&'_')
1416-
{
1417-
return self.tokenizer_error(chars.location(), "Unexpected character '_'");
1418-
}
1419-
14201389
// Consume fractional digits.
1421-
s += &peeking_next_take_while(chars, |ch, next_ch| {
1422-
ch.is_ascii_digit() || is_number_separator(ch, next_ch)
1423-
});
1424-
1425-
if self.dialect.supports_numeric_literal_underscores()
1426-
&& chars.peek() == Some(&'_')
1427-
{
1428-
return self.tokenizer_error(chars.location(), "Unexpected character '_'");
1429-
}
1390+
s += &self.tokenize_number_part(chars, |ch| ch.is_ascii_digit())?;
14301391

14311392
// No fraction -> Token::Period
14321393
if s == "." {
@@ -1450,12 +1411,16 @@ impl<'a> Tokenizer<'a> {
14501411

14511412
match char_clone.peek() {
14521413
// Definitely an exponent, get original iterator up to speed and use it
1453-
Some(&c) if c.is_ascii_digit() => {
1414+
Some(&c)
1415+
if c.is_ascii_digit()
1416+
|| (c == '_'
1417+
&& self.dialect.supports_numeric_literal_underscores()) =>
1418+
{
14541419
for _ in 0..exponent_part.len() {
14551420
chars.next();
14561421
}
14571422
exponent_part +=
1458-
&peeking_take_while(chars, |ch| ch.is_ascii_digit());
1423+
&self.tokenize_number_part(chars, |ch| ch.is_ascii_digit())?;
14591424
s += exponent_part.as_str();
14601425
}
14611426
// Not an exponent, discard the work done
@@ -2060,6 +2025,33 @@ impl<'a> Tokenizer<'a> {
20602025
})
20612026
}
20622027

2028+
fn tokenize_number_part(
2029+
&self,
2030+
chars: &mut State,
2031+
is_digit: impl Fn(char) -> bool,
2032+
) -> Result<String, TokenizerError> {
2033+
let supports_separator = self.dialect.supports_numeric_literal_underscores();
2034+
let mut s = String::new();
2035+
2036+
while let Some(&ch) = chars.peek() {
2037+
if is_digit(ch) {
2038+
chars.next();
2039+
s.push(ch);
2040+
} else if supports_separator && ch == '_' {
2041+
let next_char = chars.peekable.clone().nth(1);
2042+
if s.is_empty() || !next_char.is_some_and(&is_digit) {
2043+
return self.tokenizer_error(chars.location(), "Unexpected character '_'");
2044+
}
2045+
chars.next();
2046+
s.push(ch);
2047+
} else {
2048+
break;
2049+
}
2050+
}
2051+
2052+
Ok(s)
2053+
}
2054+
20632055
// Consume characters until newline
20642056
fn tokenize_single_line_comment(&self, chars: &mut State) -> String {
20652057
peeking_take_while(chars, |ch| match ch {
@@ -2436,24 +2428,6 @@ fn peeking_take_while(chars: &mut State, mut predicate: impl FnMut(char) -> bool
24362428
s
24372429
}
24382430

2439-
/// Same as peeking_take_while, but also passes the next character to the predicate.
2440-
fn peeking_next_take_while(
2441-
chars: &mut State,
2442-
mut predicate: impl FnMut(char, Option<char>) -> bool,
2443-
) -> String {
2444-
let mut s = String::new();
2445-
while let Some(&ch) = chars.peek() {
2446-
let next_char = chars.peekable.clone().nth(1);
2447-
if predicate(ch, next_char) {
2448-
chars.next(); // consume
2449-
s.push(ch);
2450-
} else {
2451-
break;
2452-
}
2453-
}
2454-
s
2455-
}
2456-
24572431
fn unescape_single_quoted_string(chars: &mut State<'_>) -> Option<String> {
24582432
Unescape::new(chars).unescape()
24592433
}
@@ -2785,7 +2759,7 @@ mod tests {
27852759
all_dialects_where(|dialect| dialect.supports_numeric_literal_underscores());
27862760

27872761
numeric_underscore_dialects.tokenizes_to(
2788-
"SELECT 10_000, _10_000, 1_000.123, 1_000.123_456",
2762+
"SELECT 10_000, _10_000, 1_000.123, 1_000.123_456, 1e1_0",
27892763
vec![
27902764
Token::make_keyword("SELECT"),
27912765
Token::Whitespace(Whitespace::Space),
@@ -2799,15 +2773,29 @@ mod tests {
27992773
Token::Comma,
28002774
Token::Whitespace(Whitespace::Space),
28012775
Token::Number("1_000.123_456".to_string(), false), // with an underscore in the decimal digits
2776+
Token::Comma,
2777+
Token::Whitespace(Whitespace::Space),
2778+
Token::Number("1e1_0".to_string(), false), // with an underscore in the exponent
28022779
],
28032780
);
28042781

2782+
numeric_underscore_dialects.tokenizes_to(
2783+
"0xFF_FF",
2784+
vec![Token::HexStringLiteral("FF_FF".to_string())],
2785+
);
2786+
28052787
for dialect in &numeric_underscore_dialects.dialects {
28062788
for sql in [
28072789
"SELECT 10_00_",
28082790
"SELECT 10___0",
28092791
"SELECT 1_000.123_",
28102792
"SELECT 1._000",
2793+
"SELECT 1_a",
2794+
"SELECT 1e_1",
2795+
"SELECT 1e1_",
2796+
"SELECT 1e1__0",
2797+
"SELECT 0x_1",
2798+
"SELECT 0x1_",
28112799
] {
28122800
let err = Tokenizer::new(&**dialect, sql).tokenize().unwrap_err();
28132801
assert_eq!("Unexpected character '_'", err.message);

tests/sqlparser_common.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ fn parse_numeric_literal_underscore() {
7979
("SELECT 10__00", 10),
8080
("SELECT 10_00_", 13),
8181
("SELECT 1._000", 10),
82+
("SELECT 1_a", 9),
83+
("SELECT 1e_1", 10),
84+
("SELECT 1e1_", 11),
8285
] {
8386
let err = dialects.parse_sql_statements(sql).unwrap_err();
8487
assert_eq!(

0 commit comments

Comments
 (0)