From d5e3d73c0b0c4c7387e53a13a7fa0599c07125b3 Mon Sep 17 00:00:00 2001 From: sundyli <543950155@qq.com> Date: Mon, 30 Mar 2026 00:57:06 +0000 Subject: [PATCH 1/3] fix(ast): treat X'...' as binary literal (#19600) --- src/query/ast/src/ast/expr.rs | 4 ++++ src/query/ast/src/parser/expr.rs | 8 ++++---- src/query/ast/tests/it/parser.rs | 1 + src/query/ast/tests/it/testdata/expr-error.txt | 13 +++++++++++++ src/query/sql/src/planner/semantic/type_check.rs | 2 +- src/query/sql/test-support/src/expr_parser.rs | 1 + .../suites/query/functions/binary_format.test | 11 +++++++++++ 7 files changed, 35 insertions(+), 5 deletions(-) diff --git a/src/query/ast/src/ast/expr.rs b/src/query/ast/src/ast/expr.rs index dc6b1f4b244ce..bd8f9e349ff09 100644 --- a/src/query/ast/src/ast/expr.rs +++ b/src/query/ast/src/ast/expr.rs @@ -1027,6 +1027,7 @@ pub enum Literal { precision: u8, scale: u8, }, + Binary(Vec), // Quoted string literal value String(String), Binary(Vec), @@ -1075,6 +1076,9 @@ impl Display for Literal { write!(f, "{s}") } } + Literal::Binary(val) => { + write!(f, "X'{}'", hex::encode_upper(val)) + } Literal::String(val) => { write!(f, "{}", QuotedString(val, '\'')) } diff --git a/src/query/ast/src/parser/expr.rs b/src/query/ast/src/parser/expr.rs index 650b0b021faad..6d4cf51374a2d 100644 --- a/src/query/ast/src/parser/expr.rs +++ b/src/query/ast/src/parser/expr.rs @@ -1604,7 +1604,7 @@ pub fn expr_element(i: Input) -> IResult> { }) }, ); - let hex_uint = map_res(mysql_literal_hex_str, |str| { + let mysql_hex_uint = map_res(mysql_literal_hex_str, |str| { Ok(ExprElement::Literal { value: parse_uint(str, 16).map_err(nom::Err::Failure)?, }) @@ -1767,7 +1767,7 @@ pub fn expr_element(i: Input) -> IResult> { LiteralCodeString => with_span!(code_string).parse(i), LiteralInteger => with_span!(decimal_uint).parse(i), LiteralFloat => with_span!(rule!{ #decimal_float | #dot_number_map_access }).parse(i), - MySQLLiteralHex => with_span!(hex_uint).parse(i), + MySQLLiteralHex => with_span!(mysql_hex_uint).parse(i), PGLiteralHex => with_span!(binary).parse(i), TRUE | FALSE => with_span!(boolean).parse(i), NULL => with_span!(null).parse(i), @@ -1938,7 +1938,7 @@ pub fn literal(i: Input) -> IResult { }, |token| parse_binary(token.text()).map_err(nom::Err::Failure), ); - let mut hex_uint = map_res(mysql_literal_hex_str, |str| { + let mut mysql_hex_uint = map_res(mysql_literal_hex_str, |str| { parse_uint(str, 16).map_err(nom::Err::Failure) }); let mut decimal_float = map_res( @@ -1953,7 +1953,7 @@ pub fn literal(i: Input) -> IResult { LiteralCodeString => code_string.parse(i), LiteralInteger => decimal_uint.parse(i), LiteralFloat => decimal_float.parse(i), - MySQLLiteralHex => hex_uint(i), + MySQLLiteralHex => mysql_hex_uint.parse(i), PGLiteralHex => binary.parse(i), TRUE | FALSE => boolean.parse(i), NULL => null.parse(i), diff --git a/src/query/ast/tests/it/parser.rs b/src/query/ast/tests/it/parser.rs index a6c6a8807338c..c4e2d4fb4720b 100644 --- a/src/query/ast/tests/it/parser.rs +++ b/src/query/ast/tests/it/parser.rs @@ -1560,6 +1560,7 @@ fn test_expr_error() { "#, r#"CAST(1 AS STRING) ESCAPE '$'"#, r#"1 + 1 ESCAPE '$'"#, + r#"x'ABC'"#, ]; for case in cases { diff --git a/src/query/ast/tests/it/testdata/expr-error.txt b/src/query/ast/tests/it/testdata/expr-error.txt index 6c826ef40fffe..34cd6e4c9762b 100644 --- a/src/query/ast/tests/it/testdata/expr-error.txt +++ b/src/query/ast/tests/it/testdata/expr-error.txt @@ -134,3 +134,16 @@ error: | while parsing expression +---------- Input ---------- +x'ABC' +---------- Output --------- +error: + --> SQL:1:1 + | +1 | x'ABC' + | ^^^^^^ + | | + | expecting ``, '', '', '', 'TRUE', 'FALSE', or more ... + | while parsing expression + + diff --git a/src/query/sql/src/planner/semantic/type_check.rs b/src/query/sql/src/planner/semantic/type_check.rs index 9d94f00e486c3..1c4569d7c9384 100644 --- a/src/query/sql/src/planner/semantic/type_check.rs +++ b/src/query/sql/src/planner/semantic/type_check.rs @@ -5471,8 +5471,8 @@ impl<'a> TypeChecker<'a> { DecimalSize::new_unchecked(*precision, *scale), )), Literal::Float64(float) => Scalar::Number(NumberScalar::Float64((*float).into())), - Literal::String(string) => Scalar::String(string.clone()), Literal::Binary(bytes) => Scalar::Binary(bytes.clone()), + Literal::String(string) => Scalar::String(string.clone()), Literal::Boolean(boolean) => Scalar::Boolean(*boolean), Literal::Null => Scalar::Null, }; diff --git a/src/query/sql/test-support/src/expr_parser.rs b/src/query/sql/test-support/src/expr_parser.rs index 6dbbac289c04f..cffe6670897b8 100644 --- a/src/query/sql/test-support/src/expr_parser.rs +++ b/src/query/sql/test-support/src/expr_parser.rs @@ -732,6 +732,7 @@ fn transform_literal(lit: ASTLiteral) -> Scalar { i256(value), DecimalSize::new_unchecked(precision, scale), )), + ASTLiteral::Binary(b) => Scalar::Binary(b), ASTLiteral::String(s) => Scalar::String(s), ASTLiteral::Binary(b) => Scalar::Binary(b), ASTLiteral::Boolean(b) => Scalar::Boolean(b), diff --git a/tests/sqllogictests/suites/query/functions/binary_format.test b/tests/sqllogictests/suites/query/functions/binary_format.test index a2b138f75ac98..64ac767d897f3 100644 --- a/tests/sqllogictests/suites/query/functions/binary_format.test +++ b/tests/sqllogictests/suites/query/functions/binary_format.test @@ -1,6 +1,17 @@ statement ok drop table if exists fmt_bin +statement ok +set binary_output_format = 'hex' + +query TT +select typeof(X'ABCD'), X'ABCD' +---- +Binary ABCD + +statement error +select X'0A' + 1 + statement ok create table fmt_bin(id int, v binary) From 7ee7a4136370c098aa14dcb4759a903546a5f779 Mon Sep 17 00:00:00 2001 From: sundyli <543950155@qq.com> Date: Wed, 1 Apr 2026 00:38:49 +0000 Subject: [PATCH 2/3] fix(ast): clean binary literal rebase duplicates --- src/query/ast/src/ast/expr.rs | 4 ---- src/query/sql/test-support/src/expr_parser.rs | 1 - 2 files changed, 5 deletions(-) diff --git a/src/query/ast/src/ast/expr.rs b/src/query/ast/src/ast/expr.rs index bd8f9e349ff09..dc6b1f4b244ce 100644 --- a/src/query/ast/src/ast/expr.rs +++ b/src/query/ast/src/ast/expr.rs @@ -1027,7 +1027,6 @@ pub enum Literal { precision: u8, scale: u8, }, - Binary(Vec), // Quoted string literal value String(String), Binary(Vec), @@ -1076,9 +1075,6 @@ impl Display for Literal { write!(f, "{s}") } } - Literal::Binary(val) => { - write!(f, "X'{}'", hex::encode_upper(val)) - } Literal::String(val) => { write!(f, "{}", QuotedString(val, '\'')) } diff --git a/src/query/sql/test-support/src/expr_parser.rs b/src/query/sql/test-support/src/expr_parser.rs index cffe6670897b8..6dbbac289c04f 100644 --- a/src/query/sql/test-support/src/expr_parser.rs +++ b/src/query/sql/test-support/src/expr_parser.rs @@ -732,7 +732,6 @@ fn transform_literal(lit: ASTLiteral) -> Scalar { i256(value), DecimalSize::new_unchecked(precision, scale), )), - ASTLiteral::Binary(b) => Scalar::Binary(b), ASTLiteral::String(s) => Scalar::String(s), ASTLiteral::Binary(b) => Scalar::Binary(b), ASTLiteral::Boolean(b) => Scalar::Boolean(b), From 069b06adb8cc4d2b23c63df653ecd31bda6314c4 Mon Sep 17 00:00:00 2001 From: sundyli <543950155@qq.com> Date: Wed, 1 Apr 2026 00:40:58 +0000 Subject: [PATCH 3/3] test(ast): cover malformed and arithmetic hex literals --- src/query/ast/src/parser/expr.rs | 8 ++++---- src/query/sql/src/planner/semantic/type_check.rs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/query/ast/src/parser/expr.rs b/src/query/ast/src/parser/expr.rs index 6d4cf51374a2d..650b0b021faad 100644 --- a/src/query/ast/src/parser/expr.rs +++ b/src/query/ast/src/parser/expr.rs @@ -1604,7 +1604,7 @@ pub fn expr_element(i: Input) -> IResult> { }) }, ); - let mysql_hex_uint = map_res(mysql_literal_hex_str, |str| { + let hex_uint = map_res(mysql_literal_hex_str, |str| { Ok(ExprElement::Literal { value: parse_uint(str, 16).map_err(nom::Err::Failure)?, }) @@ -1767,7 +1767,7 @@ pub fn expr_element(i: Input) -> IResult> { LiteralCodeString => with_span!(code_string).parse(i), LiteralInteger => with_span!(decimal_uint).parse(i), LiteralFloat => with_span!(rule!{ #decimal_float | #dot_number_map_access }).parse(i), - MySQLLiteralHex => with_span!(mysql_hex_uint).parse(i), + MySQLLiteralHex => with_span!(hex_uint).parse(i), PGLiteralHex => with_span!(binary).parse(i), TRUE | FALSE => with_span!(boolean).parse(i), NULL => with_span!(null).parse(i), @@ -1938,7 +1938,7 @@ pub fn literal(i: Input) -> IResult { }, |token| parse_binary(token.text()).map_err(nom::Err::Failure), ); - let mut mysql_hex_uint = map_res(mysql_literal_hex_str, |str| { + let mut hex_uint = map_res(mysql_literal_hex_str, |str| { parse_uint(str, 16).map_err(nom::Err::Failure) }); let mut decimal_float = map_res( @@ -1953,7 +1953,7 @@ pub fn literal(i: Input) -> IResult { LiteralCodeString => code_string.parse(i), LiteralInteger => decimal_uint.parse(i), LiteralFloat => decimal_float.parse(i), - MySQLLiteralHex => mysql_hex_uint.parse(i), + MySQLLiteralHex => hex_uint(i), PGLiteralHex => binary.parse(i), TRUE | FALSE => boolean.parse(i), NULL => null.parse(i), diff --git a/src/query/sql/src/planner/semantic/type_check.rs b/src/query/sql/src/planner/semantic/type_check.rs index 1c4569d7c9384..9d94f00e486c3 100644 --- a/src/query/sql/src/planner/semantic/type_check.rs +++ b/src/query/sql/src/planner/semantic/type_check.rs @@ -5471,8 +5471,8 @@ impl<'a> TypeChecker<'a> { DecimalSize::new_unchecked(*precision, *scale), )), Literal::Float64(float) => Scalar::Number(NumberScalar::Float64((*float).into())), - Literal::Binary(bytes) => Scalar::Binary(bytes.clone()), Literal::String(string) => Scalar::String(string.clone()), + Literal::Binary(bytes) => Scalar::Binary(bytes.clone()), Literal::Boolean(boolean) => Scalar::Boolean(*boolean), Literal::Null => Scalar::Null, };