Skip to content

Commit b22bf61

Browse files
PostgreSQL: Parse hexadecimal, octal and binary integer literals (#2758)
1 parent c45d1f4 commit b22bf61

6 files changed

Lines changed: 146 additions & 6 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
SELECT 0x1F, 0X_1F, 0o17, 0b1_01, X'1F'

‎src/dialect/mod.rs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,13 @@ pub trait Dialect: Debug + Any {
608608
false
609609
}
610610

611+
/// Returns true if the dialect supports integer literals in hexadecimal,
612+
/// octal and binary, e.g. `0x1F`, `0o17` and `0b101`, which then parse as
613+
/// numbers instead of `0x` hexadecimal strings.
614+
fn supports_non_decimal_integer_literals(&self) -> bool {
615+
false
616+
}
617+
611618
/// Returns true if the dialects supports specifying null treatment
612619
/// as part of a window function's parameter list as opposed
613620
/// to after the parameter list.

‎src/dialect/postgresql.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,11 @@ impl Dialect for PostgreSqlDialect {
490490
true
491491
}
492492

493+
/// See: <https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-CONSTANTS-NUMERIC>
494+
fn supports_non_decimal_integer_literals(&self) -> bool {
495+
true
496+
}
497+
493498
/// See: <https://www.postgresql.org/docs/current/arrays.html#ARRAYS-DECLARATION>
494499
fn supports_array_typedef_with_brackets(&self) -> bool {
495500
true

‎src/parser/mod.rs‎

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5135,6 +5135,30 @@ impl<'a> Parser<'a> {
51355135
})
51365136
}
51375137

5138+
#[cfg(not(feature = "bigdecimal"))]
5139+
fn parse_number_literal(s: String, _loc: Location) -> Result<String, ParserError> {
5140+
Ok(s)
5141+
}
5142+
5143+
#[cfg(feature = "bigdecimal")]
5144+
fn parse_number_literal(
5145+
s: String,
5146+
loc: Location,
5147+
) -> Result<bigdecimal::BigDecimal, ParserError> {
5148+
let radix = match s.get(..2) {
5149+
Some("0x" | "0X") => 16,
5150+
Some("0o" | "0O") => 8,
5151+
Some("0b" | "0B") => 2,
5152+
_ => return Self::parse(s, loc),
5153+
};
5154+
let digits = s[2..].strip_prefix('_').unwrap_or(&s[2..]);
5155+
bigdecimal::num_bigint::BigInt::parse_bytes(digits.as_bytes(), radix)
5156+
.map(bigdecimal::BigDecimal::from)
5157+
.ok_or_else(|| {
5158+
ParserError::ParserError(format!("Could not parse '{s}' as an integer{loc}"))
5159+
})
5160+
}
5161+
51385162
/// Parse a comma-separated list of 1+ SelectItem
51395163
pub fn parse_projection(&mut self) -> Result<Vec<SelectItem>, ParserError> {
51405164
// BigQuery and Snowflake allow trailing commas, but only in project lists
@@ -12990,10 +13014,10 @@ impl<'a> Parser<'a> {
1299013014
},
1299113015
),
1299213016
},
12993-
// The call to n.parse() returns a bigdecimal when the
12994-
// bigdecimal feature is enabled, and is otherwise a no-op
12995-
// (i.e., it returns the input string).
12996-
Token::Number(n, l) => ok_value(Value::Number(Self::parse(n, span.start)?, l)),
13017+
// A BigDecimal with the bigdecimal feature, the literal as written otherwise.
13018+
Token::Number(n, l) => {
13019+
ok_value(Value::Number(Self::parse_number_literal(n, span.start)?, l))
13020+
}
1299713021
Token::SingleQuotedString(ref s) => ok_value(Value::SingleQuotedString(
1299813022
self.maybe_concat_string_literal(s.to_string()),
1299913023
)),

‎src/tokenizer.rs‎

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1606,6 +1606,20 @@ impl<'a> Tokenizer<'a> {
16061606
return Ok(Some(Token::Number(s, false)));
16071607
}
16081608

1609+
if s == "0" && self.dialect.supports_non_decimal_integer_literals() {
1610+
let radix = match chars.peek() {
1611+
Some('x' | 'X') => Some((16, "a hexadecimal")),
1612+
Some('o' | 'O') => Some((8, "an octal")),
1613+
Some('b' | 'B') => Some((2, "a binary")),
1614+
_ => None,
1615+
};
1616+
if let Some((radix, name)) = radix {
1617+
return self
1618+
.tokenize_non_decimal_integer(chars, radix, name)
1619+
.map(Some);
1620+
}
1621+
}
1622+
16091623
// match binary literal that starts with 0x
16101624
if s == "0" && chars.peek() == Some(&'x') {
16111625
chars.next();
@@ -2302,6 +2316,32 @@ impl<'a> Tokenizer<'a> {
23022316
Ok(s)
23032317
}
23042318

2319+
/// Tokenizes an integer literal such as `0x1F` after its leading `0`,
2320+
/// keeping the spelling as written.
2321+
fn tokenize_non_decimal_integer(
2322+
&self,
2323+
chars: &mut State,
2324+
radix: u32,
2325+
name: &str,
2326+
) -> Result<Token, TokenizerError> {
2327+
let mut s = String::from('0');
2328+
s.extend(chars.next());
2329+
// An underscore may directly follow the prefix, as in `0x_1F`.
2330+
if chars.peek() == Some(&'_') && self.dialect.supports_numeric_literal_underscores() {
2331+
chars.next();
2332+
s.push('_');
2333+
}
2334+
let digits = self.tokenize_number_part(chars, |ch| ch.is_digit(radix))?;
2335+
if digits.is_empty() {
2336+
return self.tokenizer_error(
2337+
chars.location(),
2338+
format!("Expected {name} digit after '{}'", &s[..2]),
2339+
);
2340+
}
2341+
s.push_str(&digits);
2342+
Ok(Token::Number(s, false))
2343+
}
2344+
23052345
// Consume characters until newline
23062346
fn tokenize_single_line_comment(&self, chars: &mut State) -> String {
23072347
peeking_take_while(chars, |ch| match ch {
@@ -3035,10 +3075,20 @@ mod tests {
30353075
],
30363076
);
30373077

3038-
numeric_underscore_dialects.tokenizes_to(
3078+
let hex_string_dialects = all_dialects_where(|dialect| {
3079+
dialect.supports_numeric_literal_underscores()
3080+
&& !dialect.supports_non_decimal_integer_literals()
3081+
});
3082+
hex_string_dialects.tokenizes_to(
30393083
"0xFF_FF",
30403084
vec![Token::HexStringLiteral("FF_FF".to_string())],
30413085
);
3086+
for dialect in &hex_string_dialects.dialects {
3087+
let err = Tokenizer::new(&**dialect, "SELECT 0x_1")
3088+
.tokenize()
3089+
.unwrap_err();
3090+
assert_eq!("Unexpected character '_'", err.message);
3091+
}
30423092

30433093
for dialect in &numeric_underscore_dialects.dialects {
30443094
for sql in [
@@ -3050,7 +3100,6 @@ mod tests {
30503100
"SELECT 1e_1",
30513101
"SELECT 1e1_",
30523102
"SELECT 1e1__0",
3053-
"SELECT 0x_1",
30543103
"SELECT 0x1_",
30553104
] {
30563105
let err = Tokenizer::new(&**dialect, sql).tokenize().unwrap_err();

‎tests/sqlparser_postgres.rs‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11351,3 +11351,57 @@ fn parse_drop_foreign_table_rejects_modifiers() {
1135111351
);
1135211352
}
1135311353
}
11354+
11355+
#[test]
11356+
fn parse_non_decimal_integer_literals() {
11357+
let sql = "SELECT 0x1F, 0X1F, 0o17, 0O17, 0b101, 0B101, 0x_1F, 0xFF_FF, 0xFFFFFFFFFFFFFFFFFFFF";
11358+
let canonical = if cfg!(feature = "bigdecimal") {
11359+
"SELECT 31, 31, 15, 15, 5, 5, 31, 65535, 1208925819614629174706175"
11360+
} else {
11361+
sql
11362+
};
11363+
let select = pg().verified_only_select_with_canonical(sql, canonical);
11364+
for item in &select.projection {
11365+
assert!(
11366+
matches!(
11367+
item,
11368+
SelectItem::UnnamedExpr(Expr::Value(ValueWithSpan {
11369+
value: Value::Number(_, false),
11370+
..
11371+
}))
11372+
),
11373+
"{item:?}"
11374+
);
11375+
}
11376+
11377+
assert_eq!(
11378+
expr_from_projection(&pg().verified_only_select("SELECT X'1F'").projection[0]),
11379+
&Expr::Value(Value::HexStringLiteral("1F".to_string()).with_empty_span())
11380+
);
11381+
11382+
for (sql, message) in [
11383+
("SELECT 0x", "Expected a hexadecimal digit after '0x'"),
11384+
("SELECT 0Xg", "Expected a hexadecimal digit after '0X'"),
11385+
("SELECT 0o8", "Expected an octal digit after '0o'"),
11386+
("SELECT 0b2", "Expected a binary digit after '0b'"),
11387+
("SELECT 0x_", "Expected a hexadecimal digit after '0x'"),
11388+
("SELECT 0x__1", "Unexpected character '_'"),
11389+
("SELECT 0x1_", "Unexpected character '_'"),
11390+
] {
11391+
let err = pg().parse_sql_statements(sql).unwrap_err().to_string();
11392+
assert!(err.contains(message), "{sql}: {err}");
11393+
}
11394+
}
11395+
11396+
#[cfg(feature = "bigdecimal")]
11397+
#[test]
11398+
fn parse_non_decimal_integer_token_with_invalid_digits() {
11399+
let err = Parser::new(&PostgreSqlDialect {})
11400+
.with_tokens(vec![Token::Number("0xZZ".to_string(), false)])
11401+
.parse_value()
11402+
.unwrap_err();
11403+
assert_eq!(
11404+
err,
11405+
ParserError::ParserError("Could not parse '0xZZ' as an integer".to_string())
11406+
);
11407+
}

0 commit comments

Comments
 (0)