diff --git a/src/sqlast/value.rs b/src/sqlast/value.rs index 0cb7fab4..3b246bce 100644 --- a/src/sqlast/value.rs +++ b/src/sqlast/value.rs @@ -9,6 +9,8 @@ pub enum Value { SingleQuotedString(String), /// N'string value' NationalStringLiteral(String), + /// X'hex value' + HexStringLiteral(String), /// Boolean value true or false Boolean(bool), /// NULL value in insert statements, @@ -22,6 +24,7 @@ impl ToString for Value { Value::Double(v) => v.to_string(), Value::SingleQuotedString(v) => format!("'{}'", escape_single_quote_string(v)), Value::NationalStringLiteral(v) => format!("N'{}'", v), + Value::HexStringLiteral(v) => format!("X'{}'", v), Value::Boolean(v) => v.to_string(), Value::Null => "NULL".to_string(), } diff --git a/src/sqlparser.rs b/src/sqlparser.rs index 85121eef..e7519621 100644 --- a/src/sqlparser.rs +++ b/src/sqlparser.rs @@ -238,7 +238,10 @@ impl Parser { expr: Box::new(self.parse_subexpr(Self::PLUS_MINUS_PREC)?), }) } - Token::Number(_) | Token::SingleQuotedString(_) | Token::NationalStringLiteral(_) => { + Token::Number(_) + | Token::SingleQuotedString(_) + | Token::NationalStringLiteral(_) + | Token::HexStringLiteral(_) => { self.prev_token(); self.parse_sql_value() } @@ -1037,6 +1040,7 @@ impl Parser { Token::NationalStringLiteral(ref s) => { Ok(Value::NationalStringLiteral(s.to_string())) } + Token::HexStringLiteral(ref s) => Ok(Value::HexStringLiteral(s.to_string())), _ => parser_err!(format!("Unsupported value: {:?}", t)), }, None => parser_err!("Expecting a value, but found EOF"), diff --git a/src/sqltokenizer.rs b/src/sqltokenizer.rs index 70a310af..e8b34597 100644 --- a/src/sqltokenizer.rs +++ b/src/sqltokenizer.rs @@ -37,6 +37,8 @@ pub enum Token { SingleQuotedString(String), /// "National" string literal: i.e: N'string' NationalStringLiteral(String), + /// Hexadecimal string literal: i.e.: X'deadbeef' + HexStringLiteral(String), /// Comma Comma, /// Whitespace (space, tab, etc) @@ -97,6 +99,7 @@ impl ToString for Token { Token::Char(ref c) => c.to_string(), Token::SingleQuotedString(ref s) => format!("'{}'", s), Token::NationalStringLiteral(ref s) => format!("N'{}'", s), + Token::HexStringLiteral(ref s) => format!("X'{}'", s), Token::Comma => ",".to_string(), Token::Whitespace(ws) => ws.to_string(), Token::Eq => "=".to_string(), @@ -286,6 +289,23 @@ impl<'a> Tokenizer<'a> { } } } + // The spec only allows an uppercase 'X' to introduce a hex + // string, but PostgreSQL, at least, allows a lowercase 'x' too. + x @ 'x' | x @ 'X' => { + chars.next(); // consume, to check the next char + match chars.peek() { + Some('\'') => { + // X'...' - a + let s = self.tokenize_single_quoted_string(chars); + Ok(Some(Token::HexStringLiteral(s))) + } + _ => { + // regular identifier starting with an "X" + let s = self.tokenize_word(x, chars); + Ok(Some(Token::make_word(&s, None))) + } + } + } // identifier or keyword ch if self.dialect.is_identifier_start(ch) => { chars.next(); // consume the first char diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index ab5ce4d2..2098b7d1 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -923,9 +923,9 @@ fn parse_aggregate_with_group_by() { #[test] fn parse_literal_string() { - let sql = "SELECT 'one', N'national string'"; + let sql = "SELECT 'one', N'national string', X'deadBEEF'"; let select = verified_only_select(sql); - assert_eq!(2, select.projection.len()); + assert_eq!(3, select.projection.len()); assert_eq!( &ASTNode::SQLValue(Value::SingleQuotedString("one".to_string())), expr_from_projection(&select.projection[0]) @@ -934,6 +934,12 @@ fn parse_literal_string() { &ASTNode::SQLValue(Value::NationalStringLiteral("national string".to_string())), expr_from_projection(&select.projection[1]) ); + assert_eq!( + &ASTNode::SQLValue(Value::HexStringLiteral("deadBEEF".to_string())), + expr_from_projection(&select.projection[2]) + ); + + one_statement_parses_to("SELECT x'deadBEEF'", "SELECT X'deadBEEF'"); } #[test]