Skip to content

Commit e436249

Browse files
authored
Reject invalid numeric underscore separators (#2426)
1 parent 7076b79 commit e436249

2 files changed

Lines changed: 82 additions & 47 deletions

File tree

src/tokenizer.rs

Lines changed: 67 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1360,24 +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-
});
1363+
let mut s = self.tokenize_number_part(chars, |ch| ch.is_ascii_digit())?;
13741364

13751365
// match binary literal that starts with 0x
13761366
if s == "0" && chars.peek() == Some(&'x') {
13771367
chars.next();
1378-
let s2 = peeking_next_take_while(chars, |ch, next_ch| {
1379-
ch.is_ascii_hexdigit() || is_number_separator(ch, next_ch)
1380-
});
1368+
let s2 = self.tokenize_number_part(chars, |ch| ch.is_ascii_hexdigit())?;
13811369
return Ok(Some(Token::HexStringLiteral(s2)));
13821370
}
13831371

@@ -1399,9 +1387,7 @@ impl<'a> Tokenizer<'a> {
13991387
}
14001388

14011389
// Consume fractional digits.
1402-
s += &peeking_next_take_while(chars, |ch, next_ch| {
1403-
ch.is_ascii_digit() || is_number_separator(ch, next_ch)
1404-
});
1390+
s += &self.tokenize_number_part(chars, |ch| ch.is_ascii_digit())?;
14051391

14061392
// No fraction -> Token::Period
14071393
if s == "." {
@@ -1425,12 +1411,16 @@ impl<'a> Tokenizer<'a> {
14251411

14261412
match char_clone.peek() {
14271413
// Definitely an exponent, get original iterator up to speed and use it
1428-
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+
{
14291419
for _ in 0..exponent_part.len() {
14301420
chars.next();
14311421
}
14321422
exponent_part +=
1433-
&peeking_take_while(chars, |ch| ch.is_ascii_digit());
1423+
&self.tokenize_number_part(chars, |ch| ch.is_ascii_digit())?;
14341424
s += exponent_part.as_str();
14351425
}
14361426
// Not an exponent, discard the work done
@@ -2035,6 +2025,33 @@ impl<'a> Tokenizer<'a> {
20352025
})
20362026
}
20372027

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+
20382055
// Consume characters until newline
20392056
fn tokenize_single_line_comment(&self, chars: &mut State) -> String {
20402057
peeking_take_while(chars, |ch| match ch {
@@ -2411,24 +2428,6 @@ fn peeking_take_while(chars: &mut State, mut predicate: impl FnMut(char) -> bool
24112428
s
24122429
}
24132430

2414-
/// Same as peeking_take_while, but also passes the next character to the predicate.
2415-
fn peeking_next_take_while(
2416-
chars: &mut State,
2417-
mut predicate: impl FnMut(char, Option<char>) -> bool,
2418-
) -> String {
2419-
let mut s = String::new();
2420-
while let Some(&ch) = chars.peek() {
2421-
let next_char = chars.peekable.clone().nth(1);
2422-
if predicate(ch, next_char) {
2423-
chars.next(); // consume
2424-
s.push(ch);
2425-
} else {
2426-
break;
2427-
}
2428-
}
2429-
s
2430-
}
2431-
24322431
fn unescape_single_quoted_string(chars: &mut State<'_>) -> Option<String> {
24332432
Unescape::new(chars).unescape()
24342433
}
@@ -2756,8 +2755,11 @@ mod tests {
27562755
];
27572756
compare(expected, tokens);
27582757

2759-
all_dialects_where(|dialect| dialect.supports_numeric_literal_underscores()).tokenizes_to(
2760-
"SELECT 10_000, _10_000, 10_00_, 10___0, 1_000.123, 1_000.123_456",
2758+
let numeric_underscore_dialects =
2759+
all_dialects_where(|dialect| dialect.supports_numeric_literal_underscores());
2760+
2761+
numeric_underscore_dialects.tokenizes_to(
2762+
"SELECT 10_000, _10_000, 1_000.123, 1_000.123_456, 1e1_0",
27612763
vec![
27622764
Token::make_keyword("SELECT"),
27632765
Token::Whitespace(Whitespace::Space),
@@ -2767,20 +2769,38 @@ mod tests {
27672769
Token::make_word("_10_000", None), // leading underscore tokenizes as a word (parsed as column identifier)
27682770
Token::Comma,
27692771
Token::Whitespace(Whitespace::Space),
2770-
Token::Number("10_00".to_string(), false),
2771-
Token::make_word("_", None), // trailing underscores tokenizes as a word (syntax error in some dialects)
2772-
Token::Comma,
2773-
Token::Whitespace(Whitespace::Space),
2774-
Token::Number("10".to_string(), false),
2775-
Token::make_word("___0", None), // multiple underscores tokenizes as a word (syntax error in some dialects)
2776-
Token::Comma,
2777-
Token::Whitespace(Whitespace::Space),
27782772
Token::Number("1_000.123".to_string(), false), // with decimal digits
27792773
Token::Comma,
27802774
Token::Whitespace(Whitespace::Space),
27812775
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
27822779
],
27832780
);
2781+
2782+
numeric_underscore_dialects.tokenizes_to(
2783+
"0xFF_FF",
2784+
vec![Token::HexStringLiteral("FF_FF".to_string())],
2785+
);
2786+
2787+
for dialect in &numeric_underscore_dialects.dialects {
2788+
for sql in [
2789+
"SELECT 10_00_",
2790+
"SELECT 10___0",
2791+
"SELECT 1_000.123_",
2792+
"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_",
2799+
] {
2800+
let err = Tokenizer::new(&**dialect, sql).tokenize().unwrap_err();
2801+
assert_eq!("Unexpected character '_'", err.message);
2802+
}
2803+
}
27842804
}
27852805

27862806
#[test]

tests/sqlparser_common.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,21 @@ fn parse_numeric_literal_underscore() {
7474
select.projection,
7575
vec![UnnamedExpr(Expr::Value(number("10_000").with_empty_span()))]
7676
);
77+
78+
for (sql, column) in [
79+
("SELECT 10__00", 10),
80+
("SELECT 10_00_", 13),
81+
("SELECT 1._000", 10),
82+
("SELECT 1_a", 9),
83+
("SELECT 1e_1", 10),
84+
("SELECT 1e1_", 11),
85+
] {
86+
let err = dialects.parse_sql_statements(sql).unwrap_err();
87+
assert_eq!(
88+
format!("sql parser error: Unexpected character '_' at Line: 1, Column: {column}"),
89+
err.to_string()
90+
);
91+
}
7792
}
7893

7994
#[test]

0 commit comments

Comments
 (0)