This is an automated email from the ASF dual-hosted git repository.

morningman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new a8b95fafae8 [fix](nereids) Fold a VARBINARY to string cast to its 
bytes like BE does (#69025)
a8b95fafae8 is described below

commit a8b95fafae8242104182c88e5fca7727feced5d2
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Fri Oct 16 19:15:35 2026 +0800

    [fix](nereids) Fold a VARBINARY to string cast to its bytes like BE does 
(#69025)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: #68968, #68930
    
    Problem Summary:
    
    **In short.** FE constant folding cast a VARBINARY constant to a string
    type as the hex text of its bytes, while BE keeps the bytes. Every
    folded expression that takes a VARBINARY constant through such a cast,
    including `hex()` and `to_base64()` of a VARBINARY constant, returned a
    different result from the same expression evaluated by BE. This PR folds
    the cast to the bytes, leaves the cast for BE when no FE string literal
    can carry its result, and makes the two FE places that could not handle
    such a cast accept it; the third, in BE, is fixed by #68930. This came
    up while testing #68968.
    
    **Background.** `hex()` and `to_base64()` only have string signatures,
    so a VARBINARY argument reaches them through an implicit `CAST(... AS
    VARCHAR)`. BE implements `CAST(VARBINARY AS VARCHAR/STRING)` by keeping
    the bytes as they are. When the argument is a constant,
    `FoldConstantRuleOnFE` folds that cast with `Literal.checkedCastTo`,
    which builds the string from `VarBinaryLiteral.castValueToString()`;
    that method returned `toString()`, the base16 text of the bytes.
    
    **The problem, and what it cost.**
    
    | Query | folded on FE (master) | evaluated by BE
    (`debug_skip_fold_constant=true`) / with this PR |
    |---|---|---|
    | `SELECT hex(cast('abc' AS varbinary(16)))` | `363136323633` | `616263`
    |
    | `SELECT to_base64(cast('abc' AS varbinary(16)))` | `NjE2MjYz` | `YWJj`
    |
    | `SELECT length(cast(cast('abc' AS varbinary(16)) AS string))` | 6 | 3
    |
    | `SELECT hex(X'FF')` | `4646` | `FF` |
    | `SELECT k FROM t WHERE s = cast(X'61' AS string)` | no rows (compares
    with `'61'`) | the row with `'a'` |
    
    The plan shows the difference: master sends `hex('616263')` to BE, while
    with folding disabled it sends `hex(CAST(X'616263' AS varchar(65533)))`.
    The same wrong value appears for a column read from a constant subquery
    or a view.
    
    **How this PR fixes it.**
    
    - `VarBinaryLiteral.castValueToString()` decodes the bytes as UTF-8
    (`CodingErrorAction.REPORT`), so the folded string carries exactly the
    bytes BE keeps; valid UTF-8 round-trips unchanged when the literal is
    sent to BE.
    - For some bytes `castValueToString()` throws an `AnalysisException`,
    and `FoldConstantRuleOnFE.visitCast`, which turns only a `CastException`
    into NULL, then leaves the cast for BE to evaluate:
      - bytes that are not valid UTF-8, which a Java string cannot carry;
    - text with a NUL byte or a supplementary character, which FE evaluates
    differently from BE: `StringLikeLiteral.compareTo` takes the bytes after
    a NUL as padding, so `CAST(X'610062' AS STRING) = 'a'` would fold to
    TRUE, and `VARCHAR(n)` truncation and `trim_in` work on UTF-16 units, so
    they would split U+1F600.
    - Master always folded such a cast to the hex text, so three places had
    never met it in a plan:
    - BE: `ScanOperator` digests every conjunct for the condition cache (on
    by default), and `VLiteral::get_digest` hashed the literal's column,
    which `ColumnVarbinary` does not support. `WHERE s = CAST(X'FF' AS
    STRING)` failed with `update_xxHash_with_value is not supported for
    ColumnVarbinary`. #68930 has since made `VLiteral::get_digest` return 0
    for a VARBINARY column, which keeps such a conjunct out of the condition
    cache. This PR made the same change before it was rebased onto #68930,
    and now leaves BE unchanged.
    - FE, fixed here: `CheckAfterRewrite` rejected a hash or mark join
    condition with any VARBINARY sub-expression, so `CAST(X'610062' AS
    STRING) IN (SELECT s FROM t)` on a nullable `s` failed with `varbinary
    type could not in join equal conditions`. It now rejects a condition
    only when a comparison in it has a VARBINARY operand.
    - FE, fixed here: with `enable_fold_constant_by_be=true`, BE folded such
    a cast when it was the largest constant part of a predicate, and
    `FoldConstantRuleOnBE.getResultExpression` decoded the result as UTF-8
    with U+FFFD for invalid bytes, so `WHERE s = CAST(X'FF' AS STRING)`
    became `s = '�'`. `shouldSkipFold` now skips a cast from a type that
    contains VARBINARY, as it already skips `from_base64` for the same
    reason.
    
    **Results.** On a local cluster, with a table `t` holding `'a'`, `'a'`
    NUL `'b'`, the byte 0xFF, U+FFFD, NULL and U+1F600:
    
    | Query | master | this PR, in each of the three modes below |
    |---|---|---|
    | `WHERE s = CAST(X'FF' AS STRING)` | no rows | the 0xFF row |
    | `WHERE s = CAST(X'610062' AS STRING)` | no rows | the `'a'` NUL `'b'`
    row |
    | `WHERE s = CAST(CAST(X'F09F9880' AS STRING) AS VARCHAR(1))` | no rows
    | the U+1F600 row |
    | `SELECT CAST(X'FF' AS STRING) IN (SELECT s FROM t WHERE k <> 4)` |
    NULL | true |
    
    The three modes are FE folding (the default), FE and BE folding
    (`enable_fold_constant_by_be=true`), and no folding
    (`debug_skip_fold_constant=true`); P0 runs turn
    `enable_fold_constant_by_be` on at random. `test_varbinary_cast_fold`
    runs every query in the three modes with the condition cache on, and
    each block returns the same rows in all three. The FE UT fails when
    `castValueToString()` returns the hex text again (`expected: <'abc'> but
    was: <'616263'>`), and each of the two FE fixes above has a test that
    fails without it.
    
    The existing `.out` of
    `query_p0/sql_functions/binary_functions/test_binary_hex_function`
    recorded the folded hex text for `sql_25` (`select cast(X'61' as
    string), ...` gave `61`), while its `sql_24` already expected `a` for
    the same bytes read from a column. It is regenerated by the script. The
    `.out` of
    
`external_table_p0/dialect_compatible/sql/presto/scalar/TestVarbinaryFunctions`
    already expects the BE values (`YWJj` for `to_base64(CAST('abc' AS
    VARBINARY))`).
---
 .../nereids/rules/analysis/CheckAfterRewrite.java  | 12 ++-
 .../expression/rules/FoldConstantRuleOnBE.java     |  8 ++
 .../expressions/literal/VarBinaryLiteral.java      | 39 +++++++++-
 .../doris/nereids/util/TypeCoercionUtils.java      | 21 ++----
 .../rules/expression/FoldConstantRuleOnBETest.java | 22 ++++++
 .../nereids/rules/expression/FoldConstantTest.java | 35 +++++++++
 .../nereids/types/VarBinarySqlSupportTest.java     | 54 +++++++++++++
 .../binary_functions/test_binary_hex_function.out  |  2 +-
 .../binary_functions/test_varbinary_cast_fold.out  | 78 +++++++++++++++++++
 .../test_varbinary_cast_fold.groovy                | 88 ++++++++++++++++++++++
 10 files changed, 339 insertions(+), 20 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/CheckAfterRewrite.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/CheckAfterRewrite.java
index 8b2f19b8532..ebdbe377da1 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/CheckAfterRewrite.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/CheckAfterRewrite.java
@@ -23,6 +23,7 @@ import org.apache.doris.nereids.properties.OrderKey;
 import org.apache.doris.nereids.rules.Rule;
 import org.apache.doris.nereids.rules.RuleType;
 import org.apache.doris.nereids.trees.expressions.Alias;
+import org.apache.doris.nereids.trees.expressions.ComparisonPredicate;
 import org.apache.doris.nereids.trees.expressions.Expression;
 import org.apache.doris.nereids.trees.expressions.Match;
 import org.apache.doris.nereids.trees.expressions.Slot;
@@ -192,13 +193,13 @@ public class CheckAfterRewrite extends 
OneAnalysisRuleFactory {
         } else if (plan instanceof LogicalJoin) {
             LogicalJoin<?, ?> join = (LogicalJoin<?, ?>) plan;
             for (Expression conjunct : join.getHashJoinConjuncts()) {
-                if (conjunct.anyMatch(e -> ((Expression) 
e).getDataType().isVarBinaryType())) {
+                if (comparesVarBinary(conjunct)) {
                     throw new AnalysisException(
                             "varbinary type could not in join equal 
conditions: " + conjunct.toSql());
                 }
             }
             for (Expression conjunct : join.getMarkJoinConjuncts()) {
-                if (conjunct.anyMatch(e -> ((Expression) 
e).getDataType().isVarBinaryType())) {
+                if (comparesVarBinary(conjunct)) {
                     throw new AnalysisException(
                             "varbinary type could not in join equal 
conditions: " + conjunct.toSql());
                 }
@@ -206,6 +207,13 @@ public class CheckAfterRewrite extends 
OneAnalysisRuleFactory {
         }
     }
 
+    // BE cannot hash or compare VARBINARY values, but a join condition may 
compare strings cast from VARBINARY,
+    // such as CAST(X'FF' AS STRING) = s, so only the compared operands are 
checked.
+    private static boolean comparesVarBinary(Expression conjunct) {
+        return conjunct.anyMatch(e -> e instanceof ComparisonPredicate && 
((ComparisonPredicate) e).children()
+                .stream().anyMatch(operand -> 
operand.getDataType().isVarBinaryType()));
+    }
+
     private void checkMatchIsUsedCorrectly(Plan plan) {
         for (Expression expression : plan.getExpressions()) {
             if (expression instanceof Match) {
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/expression/rules/FoldConstantRuleOnBE.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/expression/rules/FoldConstantRuleOnBE.java
index 2c0789e01c4..d8fd83457c8 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/expression/rules/FoldConstantRuleOnBE.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/expression/rules/FoldConstantRuleOnBE.java
@@ -79,6 +79,7 @@ import org.apache.doris.nereids.types.MapType;
 import org.apache.doris.nereids.types.StructField;
 import org.apache.doris.nereids.types.StructType;
 import org.apache.doris.nereids.types.TimeStampNsType;
+import org.apache.doris.nereids.util.TypeCoercionUtils;
 import org.apache.doris.proto.InternalService;
 import org.apache.doris.proto.InternalService.PConstantExprResult;
 import org.apache.doris.proto.Types.PScalarType;
@@ -298,6 +299,13 @@ public class FoldConstantRuleOnBE implements 
ExpressionPatternRuleFactory {
                     cast.isStrict() || SessionVariable.enableStrictCast()) || 
cast.child().isNullLiteral()) {
                 return true;
             }
+            // FoldConstantRuleOnFE leaves a cast of VARBINARY bytes for BE 
when no FE string literal can carry
+            // its result (see VarBinaryLiteral.castValueToString). Folding 
the cast here would build that literal
+            // from the BE result anyway: getResultExpression decodes invalid 
UTF-8 as U+FFFD, and FE string
+            // code treats a NUL byte or a supplementary character unlike BE.
+            if 
(TypeCoercionUtils.hasVarBinaryType(cast.child().getDataType())) {
+                return true;
+            }
         }
 
         // This kind of function is often used to change the attributes of 
columns.
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/literal/VarBinaryLiteral.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/literal/VarBinaryLiteral.java
index 6813e6da409..7acfbad76da 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/literal/VarBinaryLiteral.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/literal/VarBinaryLiteral.java
@@ -18,14 +18,20 @@
 package org.apache.doris.nereids.trees.expressions.literal;
 
 import org.apache.doris.analysis.LiteralExpr;
+import org.apache.doris.nereids.exceptions.AnalysisException;
 import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
 import org.apache.doris.nereids.types.DataType;
 import org.apache.doris.nereids.types.VarBinaryType;
 
 import com.google.common.io.BaseEncoding;
 
+import java.nio.ByteBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.CodingErrorAction;
+import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
 import java.util.Locale;
+import java.util.Optional;
 
 /**
  * Represents varbinary literal
@@ -73,8 +79,39 @@ public class VarBinaryLiteral extends Literal implements 
ComparableLiteral {
         return byteValues;
     }
 
+    /**
+     * BE casts VARBINARY to a string type by keeping the bytes as they are, 
so CAST(X'616263' AS STRING) is
+     * 'abc', not the hex text '616263'. A Java string carries those bytes 
only when they are valid UTF-8; for
+     * other bytes, such as X'FF', this throws, and 
FoldConstantRuleOnFE.visitCast then leaves the cast for BE
+     * to evaluate (it turns only a CastException into NULL).
+     *
+     * <p>Valid UTF-8 with a NUL byte or a supplementary character is left for 
BE too, because FE does not
+     * evaluate such string literals like BE does: StringLikeLiteral.compareTo 
takes the bytes after a NUL for
+     * padding, so CAST(X'610062' AS STRING) = 'a' would fold to TRUE while BE 
compares all three bytes, and
+     * VARCHAR(n) truncation and trim_in work on UTF-16 units, so 
CAST(X'F09F9880' AS VARCHAR(1)) would keep
+     * half of U+1F600 where BE keeps the whole character.</p>
+     */
+    @Override
     protected String castValueToString() {
-        return toString();
+        String value = toUtf8String().orElseThrow(() -> new AnalysisException(
+                "Cannot cast VARBINARY " + computeToSql() + " to a string type 
on FE: it is not valid UTF-8"));
+        if (value.indexOf('\0') >= 0 || 
value.codePoints().anyMatch(Character::isSupplementaryCodePoint)) {
+            throw new AnalysisException("Cannot cast VARBINARY " + 
computeToSql()
+                    + " to a string type on FE: it has a NUL byte or a 
supplementary character");
+        }
+        return value;
+    }
+
+    /** The bytes decoded as UTF-8, or empty when they are not valid UTF-8 and 
only BE can cast them. */
+    public Optional<String> toUtf8String() {
+        try {
+            return Optional.of(StandardCharsets.UTF_8.newDecoder()
+                    .onMalformedInput(CodingErrorAction.REPORT)
+                    .onUnmappableCharacter(CodingErrorAction.REPORT)
+                    .decode(ByteBuffer.wrap(byteValues)).toString());
+        } catch (CharacterCodingException e) {
+            return Optional.empty();
+        }
     }
 
     @Override
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/util/TypeCoercionUtils.java 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/util/TypeCoercionUtils.java
index f8b722e40cf..1a27be89e55 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/util/TypeCoercionUtils.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/util/TypeCoercionUtils.java
@@ -387,6 +387,10 @@ public class TypeCoercionUtils {
         return hasSpecifiedType(dataType, TimeStampNsType.class);
     }
 
+    public static boolean hasVarBinaryType(DataType dataType) {
+        return hasSpecifiedType(dataType, VarBinaryType.class);
+    }
+
     private static boolean hasTimeStampNsCompatibleDateTimeType(DataType 
dataType) {
         return hasSpecifiedType(dataType, type -> type instanceof DateTimeType
                 || type instanceof DateTimeV2Type || type instanceof 
TimeStampTzType);
@@ -2933,27 +2937,12 @@ public class TypeCoercionUtils {
         // Common binary types support value selection, but BE comparison 
kernels do not.
         // Check before equal-type shortcuts or casts can hide an unsupported 
binary argument.
         for (Expression argument : arguments) {
-            if (containsVarbinary(argument.getDataType())) {
+            if (hasVarBinaryType(argument.getDataType())) {
                 throw new AnalysisException(operation + " does not support 
VARBINARY arguments");
             }
         }
     }
 
-    private static boolean containsVarbinary(DataType type) {
-        if (type instanceof ArrayType) {
-            return containsVarbinary(((ArrayType) type).getItemType());
-        }
-        if (type instanceof StructType) {
-            return ((StructType) type).getFields().stream()
-                    .anyMatch(field -> containsVarbinary(field.getDataType()));
-        }
-        if (type instanceof MapType) {
-            return containsVarbinary(((MapType) type).getKeyType())
-                    || containsVarbinary(((MapType) type).getValueType());
-        }
-        return type.isVarBinaryType();
-    }
-
     private static Optional<DataType> findCommonBinaryType(DataType left, 
DataType right) {
         if (left.isVarBinaryType() && right.isVarBinaryType()) {
             return Optional.of(VarBinaryType.createVarBinaryType(
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/expression/FoldConstantRuleOnBETest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/expression/FoldConstantRuleOnBETest.java
index 28037322ecc..3c88bec9250 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/expression/FoldConstantRuleOnBETest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/expression/FoldConstantRuleOnBETest.java
@@ -23,6 +23,7 @@ import 
org.apache.doris.nereids.rules.expression.rules.FoldConstantRuleOnBE;
 import org.apache.doris.nereids.rules.expression.rules.FoldConstantRuleOnFE;
 import org.apache.doris.nereids.trees.expressions.Add;
 import org.apache.doris.nereids.trees.expressions.And;
+import org.apache.doris.nereids.trees.expressions.Cast;
 import org.apache.doris.nereids.trees.expressions.EqualTo;
 import org.apache.doris.nereids.trees.expressions.Expression;
 import org.apache.doris.nereids.trees.expressions.IsNull;
@@ -32,8 +33,12 @@ import 
org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.Literal;
 import org.apache.doris.nereids.trees.expressions.literal.MapLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.NullLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.StringLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.StructLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.UuidLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.VarBinaryLiteral;
+import org.apache.doris.nereids.types.ArrayType;
+import org.apache.doris.nereids.types.StringType;
 import org.apache.doris.nereids.types.UuidType;
 import org.apache.doris.thrift.TExpr;
 
@@ -88,6 +93,23 @@ class FoldConstantRuleOnBETest extends 
ExpressionRewriteTestHelper {
                 collectConstants(new And(new IsNull(uuidResult), supported)));
     }
 
+    @Test
+    void testSkipCastFromVarBinary() {
+        // FE leaves these casts for BE because no FE string literal carries 
0xFF. BE folding would build such a
+        // literal from its result anyway, decoding 0xFF as U+FFFD, so they 
wait for BE to evaluate them.
+        VarBinaryLiteral notUtf8 = new VarBinaryLiteral(new byte[] {(byte) 
0xFF});
+        List<Expression> casts = ImmutableList.of(new Cast(notUtf8, 
StringType.INSTANCE),
+                new Cast(new ArrayLiteral(ImmutableList.of(notUtf8)), 
ArrayType.of(StringType.INSTANCE)));
+        for (Expression cast : casts) {
+            Assertions.assertEquals(cast, 
FoldConstantRuleOnFE.VISITOR_INSTANCE.rewrite(cast, context));
+            Assertions.assertTrue(collectConstants(cast).isEmpty(), 
cast.toSql());
+        }
+        Expression filter = new EqualTo(casts.get(0), new 
StringLiteral(String.valueOf((char) 0xFFFD)));
+        Expression supported = new EqualTo(new Add(new IntegerLiteral(1), new 
IntegerLiteral(2)),
+                new IntegerLiteral(3));
+        Assertions.assertEquals(ImmutableList.of(supported), 
collectConstants(new And(filter, supported)));
+    }
+
     private List<Expression> collectConstants(Expression expression) {
         Map<String, Expression> constants = new HashMap<>();
         Map<String, TExpr> serialized = new HashMap<>();
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/expression/FoldConstantTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/expression/FoldConstantTest.java
index ea3f7ace982..652888628a4 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/expression/FoldConstantTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/expression/FoldConstantTest.java
@@ -123,6 +123,7 @@ import 
org.apache.doris.nereids.trees.expressions.literal.StringLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.TimeStampNsLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.TimeV2Literal;
 import org.apache.doris.nereids.trees.expressions.literal.TinyIntLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.VarBinaryLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.VarcharLiteral;
 import org.apache.doris.nereids.trees.plans.RelationId;
 import org.apache.doris.nereids.types.BooleanType;
@@ -132,6 +133,7 @@ import org.apache.doris.nereids.types.DecimalV3Type;
 import org.apache.doris.nereids.types.DoubleType;
 import org.apache.doris.nereids.types.FloatType;
 import org.apache.doris.nereids.types.IntegerType;
+import org.apache.doris.nereids.types.StringType;
 import org.apache.doris.nereids.types.TinyIntType;
 import org.apache.doris.nereids.types.VarcharType;
 import org.apache.doris.nereids.types.VariantType;
@@ -144,6 +146,7 @@ import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
 import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
 import java.time.LocalDateTime;
 
 class FoldConstantTest extends ExpressionRewriteTestHelper {
@@ -318,6 +321,38 @@ class FoldConstantTest extends ExpressionRewriteTestHelper 
{
         Assertions.assertEquals(new DoubleLiteral(20240229123456D), rewritten);
     }
 
+    @Test
+    void testCastVarBinaryToStringFold() {
+        executor = new ExpressionRuleExecutor(ImmutableList.of(
+                bottomUp(FoldConstantRuleOnFE.VISITOR_INSTANCE)
+        ));
+
+        // BE keeps the bytes of a VARBINARY cast to a string type, so FE 
folds to those bytes, not to
+        // their hex text '616263'.
+        VarBinaryLiteral abc = new 
VarBinaryLiteral("abc".getBytes(StandardCharsets.UTF_8));
+        Assertions.assertEquals(new StringLiteral("abc"),
+                executor.rewrite(new Cast(abc, StringType.INSTANCE), context));
+        Assertions.assertEquals(new VarcharLiteral("abc", 16),
+                executor.rewrite(new Cast(abc, 
VarcharType.createVarcharType(16)), context));
+        VarBinaryLiteral multiByte = new 
VarBinaryLiteral("中文".getBytes(StandardCharsets.UTF_8));
+        Assertions.assertEquals(new StringLiteral("中文"),
+                executor.rewrite(new Cast(multiByte, StringType.INSTANCE), 
context));
+
+        // 0xFF is not valid UTF-8, so no FE string literal carries it; the 
cast is left for BE.
+        Cast notUtf8 = new Cast(new VarBinaryLiteral(new byte[] {(byte) 
0xFF}), StringType.INSTANCE);
+        Assertions.assertEquals(notUtf8, executor.rewrite(notUtf8, context));
+
+        // FE does not evaluate a NUL byte or a supplementary character like 
BE: a string comparison takes the
+        // bytes after a NUL for padding, and VARCHAR(n) truncation splits 
U+1F600 into a lone surrogate. Those
+        // casts are left for BE as well.
+        Cast withNul = new Cast(new VarBinaryLiteral(new byte[] {'a', 0, 
'b'}), StringType.INSTANCE);
+        Assertions.assertEquals(withNul, executor.rewrite(withNul, context));
+        VarBinaryLiteral grinningFace = new VarBinaryLiteral(
+                new byte[] {(byte) 0xF0, (byte) 0x9F, (byte) 0x98, (byte) 
0x80});
+        Cast supplementary = new Cast(grinningFace, 
VarcharType.createVarcharType(1));
+        Assertions.assertEquals(supplementary, executor.rewrite(supplementary, 
context));
+    }
+
     @Test
     void testFoldDate() {
         executor = new ExpressionRuleExecutor(ImmutableList.of(
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/types/VarBinarySqlSupportTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/types/VarBinarySqlSupportTest.java
index ca5c1475178..a737be6aa81 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/types/VarBinarySqlSupportTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/types/VarBinarySqlSupportTest.java
@@ -18,15 +18,34 @@
 package org.apache.doris.nereids.types;
 
 import org.apache.doris.catalog.Env;
+import org.apache.doris.nereids.exceptions.AnalysisException;
+import org.apache.doris.nereids.hint.DistributeHint;
+import org.apache.doris.nereids.rules.analysis.CheckAfterRewrite;
+import org.apache.doris.nereids.trees.expressions.Alias;
+import org.apache.doris.nereids.trees.expressions.Cast;
+import org.apache.doris.nereids.trees.expressions.EqualTo;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.MarkJoinSlotReference;
+import org.apache.doris.nereids.trees.expressions.Slot;
 import org.apache.doris.nereids.trees.expressions.literal.VarBinaryLiteral;
+import org.apache.doris.nereids.trees.plans.DistributeType;
+import org.apache.doris.nereids.trees.plans.JoinType;
+import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.RelationId;
+import org.apache.doris.nereids.trees.plans.logical.LogicalJoin;
+import org.apache.doris.nereids.trees.plans.logical.LogicalOneRowRelation;
 import org.apache.doris.nereids.util.PlanChecker;
 import org.apache.doris.qe.GlobalVariable;
 import org.apache.doris.thrift.TUniqueId;
 import org.apache.doris.utframe.TestWithFeService;
 
+import com.google.common.collect.ImmutableList;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
+import java.util.List;
+import java.util.Optional;
+
 public class VarBinarySqlSupportTest extends TestWithFeService {
     @Test
     public void testNativeVarbinaryRemainsUnsupported() {
@@ -148,6 +167,41 @@ public class VarBinarySqlSupportTest extends 
TestWithFeService {
         }
     }
 
+    @Test
+    public void testJoinConditionComparingStringsCastFromBinary() {
+        // FE leaves CAST(X'FF' AS STRING) for BE because no FE string literal 
carries 0xFF, so a nullable IN
+        // subquery keeps the cast in a mark join condition, which compares 
two strings.
+        PlanChecker.from(connectContext)
+                .analyze("select cast(X'FF' as string) in (select encoded from 
source_bytes)").rewrite();
+        PlanChecker.from(connectContext)
+                .analyze("select cast(X'610062' as string) not in (select 
encoded from source_bytes)").rewrite();
+
+        // CheckAfterRewrite rejects a join condition only when it compares 
VARBINARY values.
+        Plan left = new LogicalOneRowRelation(new RelationId(1),
+                ImmutableList.of(new Alias(new VarBinaryLiteral(new byte[] 
{(byte) 0xFF}), "l")));
+        Plan right = new LogicalOneRowRelation(new RelationId(2),
+                ImmutableList.of(new Alias(new VarBinaryLiteral(new byte[] 
{(byte) 0xFF}), "r")));
+        Slot l = left.getOutput().get(0);
+        Slot r = right.getOutput().get(0);
+        Expression strings = new EqualTo(new Cast(l, StringType.INSTANCE), new 
Cast(r, StringType.INSTANCE));
+        for (boolean mark : new boolean[] {false, true}) {
+            PlanChecker.from(connectContext, join(left, right, strings, 
mark)).applyBottomUp(new CheckAfterRewrite());
+            AnalysisException error = 
Assertions.assertThrows(AnalysisException.class,
+                    () -> PlanChecker.from(connectContext, join(left, right, 
new EqualTo(l, r), mark))
+                            .applyBottomUp(new CheckAfterRewrite()));
+            Assertions.assertTrue(error.getMessage().contains("varbinary type 
could not in join equal conditions"),
+                    error.getMessage());
+        }
+    }
+
+    private static Plan join(Plan left, Plan right, Expression condition, 
boolean mark) {
+        List<Expression> conditions = ImmutableList.of(condition);
+        return new LogicalJoin<>(mark ? JoinType.LEFT_SEMI_JOIN : 
JoinType.INNER_JOIN,
+                mark ? ImmutableList.of() : conditions, ImmutableList.of(), 
mark ? conditions : ImmutableList.of(),
+                new DistributeHint(DistributeType.NONE),
+                mark ? Optional.of(new MarkJoinSlotReference("mark")) : 
Optional.empty(), left, right, null);
+    }
+
     @Test
     public void testView() throws Exception {
         Assertions.assertEquals("X'0080FF'", new 
VarBinaryLiteral("0080FF").toSql());
diff --git 
a/regression-test/data/query_p0/sql_functions/binary_functions/test_binary_hex_function.out
 
b/regression-test/data/query_p0/sql_functions/binary_functions/test_binary_hex_function.out
index c5c5ff465dd..ef81a03d91c 100644
--- 
a/regression-test/data/query_p0/sql_functions/binary_functions/test_binary_hex_function.out
+++ 
b/regression-test/data/query_p0/sql_functions/binary_functions/test_binary_hex_function.out
@@ -104,7 +104,7 @@
 9      aaabbbcccdddeeefff      aaabbbcccdddeeefff
 
 -- !sql_25 --
-61     5A      616263647879
+a      Z       abcdxy
 
 -- !sql_26 --
 0x6162 0x48656C6C6F2C20215E25  0x446F726973
diff --git 
a/regression-test/data/query_p0/sql_functions/binary_functions/test_varbinary_cast_fold.out
 
b/regression-test/data/query_p0/sql_functions/binary_functions/test_varbinary_cast_fold.out
new file mode 100644
index 00000000000..b271c270b4e
--- /dev/null
+++ 
b/regression-test/data/query_p0/sql_functions/binary_functions/test_varbinary_cast_fold.out
@@ -0,0 +1,78 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !data --
+1      61      1
+2      610062  3
+3      FF      1
+4      EFBFBD  3
+5      \N      \N
+6      F09F9880        4
+
+-- !hex_fold --
+616263 E4B8AD  FF      
+
+-- !base64_fold --
+YWJj   /w==
+
+-- !cast_fold --
+abc    3       中       1
+
+-- !edge_fold --
+false  F09F9880        F09F9880
+
+-- !filter_fold --
+array  3
+emoji  6
+emoji_varchar1 6
+ff     3
+nul    2
+utf8   1
+
+-- !in_fold --
+true   true    false   false
+
+-- !hex_fold_by_be --
+616263 E4B8AD  FF      
+
+-- !base64_fold_by_be --
+YWJj   /w==
+
+-- !cast_fold_by_be --
+abc    3       中       1
+
+-- !edge_fold_by_be --
+false  F09F9880        F09F9880
+
+-- !filter_fold_by_be --
+array  3
+emoji  6
+emoji_varchar1 6
+ff     3
+nul    2
+utf8   1
+
+-- !in_fold_by_be --
+true   true    false   false
+
+-- !hex_no_fold --
+616263 E4B8AD  FF      
+
+-- !base64_no_fold --
+YWJj   /w==
+
+-- !cast_no_fold --
+abc    3       中       1
+
+-- !edge_no_fold --
+false  F09F9880        F09F9880
+
+-- !filter_no_fold --
+array  3
+emoji  6
+emoji_varchar1 6
+ff     3
+nul    2
+utf8   1
+
+-- !in_no_fold --
+true   true    false   false
+
diff --git 
a/regression-test/suites/query_p0/sql_functions/binary_functions/test_varbinary_cast_fold.groovy
 
b/regression-test/suites/query_p0/sql_functions/binary_functions/test_varbinary_cast_fold.groovy
new file mode 100644
index 00000000000..9aba58205d4
--- /dev/null
+++ 
b/regression-test/suites/query_p0/sql_functions/binary_functions/test_varbinary_cast_fold.groovy
@@ -0,0 +1,88 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+// A cast of a VARBINARY value to a string type keeps its bytes, so hex(), 
to_base64() and length() of a
+// VARBINARY constant, which take the value through that cast, must give the 
same results whether FE folds
+// the constant (fold), FE and BE fold it (fold_by_be) or BE evaluates it 
(no_fold); the results of a block
+// must match in the three modes. X'FF' is not valid UTF-8, so FE leaves its 
cast to BE. So does text with a
+// NUL byte or a supplementary character, which FE does not evaluate like BE 
(edge): 'a', NUL, 'b' is not equal
+// to 'a', and VARCHAR(1) and trim_in keep U+1F600 (F09F9880) whole. Such a 
cast also stays in a scan filter,
+// whose conjuncts the condition cache digests (filter), and in the mark join 
of a nullable IN subquery (in).
+// Each mode puts its name in a column alias, so that it is not served the 
cached result of another mode.
+
+suite("test_varbinary_cast_fold") {
+    sql "drop table if exists test_varbinary_cast_fold_t"
+    sql """
+        CREATE TABLE test_varbinary_cast_fold_t (k int, s string NULL)
+        DUPLICATE KEY(k) DISTRIBUTED BY HASH(k) BUCKETS 1 PROPERTIES 
('replication_num' = '1')
+    """
+    // BE evaluates the casts, so the rows hold the bytes: 'a', 'a' NUL 'b', 
0xFF, U+FFFD, NULL and U+1F600.
+    sql "set enable_fold_constant_by_be = false"
+    sql "set debug_skip_fold_constant = true"
+    sql """
+        INSERT INTO test_varbinary_cast_fold_t VALUES (1, 'a'), (2, 
cast(X'610062' AS string)),
+            (3, cast(X'FF' AS string)), (4, cast(X'EFBFBD' AS string)), (5, 
NULL), (6, cast(X'F09F9880' AS string))
+    """
+    order_qt_data "SELECT k, hex(s), length(s) FROM test_varbinary_cast_fold_t"
+
+    sql "set enable_condition_cache = true"
+    def modes = [
+            fold      : [false, false],
+            fold_by_be: [true, false],
+            no_fold   : [false, true]]
+    modes.each { mode, settings ->
+        sql "set enable_fold_constant_by_be = ${settings[0]}"
+        sql "set debug_skip_fold_constant = ${settings[1]}"
+        "order_qt_hex_${mode}" """
+            SELECT hex(cast('abc' AS varbinary(16))) AS hex_${mode}, 
hex(X'E4B8AD'), hex(X'FF'), hex(X'')
+        """
+        "order_qt_base64_${mode}" """
+            SELECT to_base64(cast('abc' AS varbinary(16))) AS base64_${mode}, 
to_base64(X'FF')
+        """
+        "order_qt_cast_${mode}" """
+            SELECT cast(cast('abc' AS varbinary(16)) AS string) AS 
cast_${mode},
+                   length(cast(cast('abc' AS varbinary(16)) AS varchar(16))),
+                   cast(X'E4B8AD' AS string),
+                   length(cast(X'FF' AS string))
+        """
+        "order_qt_edge_${mode}" """
+            SELECT cast(X'610062' AS string) = 'a' AS edge_${mode},
+                   hex(cast(X'F09F9880' AS varchar(1))),
+                   hex(trim_in(cast(X'F09F9880' AS string), cast(X'F09F9881' 
AS string)))
+        """
+        "order_qt_filter_${mode}" """
+            SELECT 'ff' AS filter_${mode}, k FROM test_varbinary_cast_fold_t 
WHERE s = cast(X'FF' AS string)
+            UNION ALL
+            SELECT 'nul', k FROM test_varbinary_cast_fold_t WHERE s = 
cast(X'610062' AS string)
+            UNION ALL
+            SELECT 'emoji', k FROM test_varbinary_cast_fold_t WHERE s = 
cast(X'F09F9880' AS string)
+            UNION ALL
+            SELECT 'emoji_varchar1', k FROM test_varbinary_cast_fold_t
+            WHERE s = cast(cast(X'F09F9880' AS string) AS varchar(1))
+            UNION ALL
+            SELECT 'array', k FROM test_varbinary_cast_fold_t WHERE 
array_contains(cast([X'FF'] AS array<string>), s)
+            UNION ALL
+            SELECT 'utf8', k FROM test_varbinary_cast_fold_t WHERE s = 
cast(X'61' AS string)
+        """
+        "order_qt_in_${mode}" """
+            SELECT cast(X'610062' AS string) IN (SELECT s FROM 
test_varbinary_cast_fold_t) AS in_${mode},
+                   cast(X'FF' AS string) IN (SELECT s FROM 
test_varbinary_cast_fold_t WHERE k <> 4),
+                   cast(X'F09F9880' AS string) NOT IN (SELECT s FROM 
test_varbinary_cast_fold_t WHERE s IS NOT NULL),
+                   cast(X'616263' AS string) IN (SELECT s FROM 
test_varbinary_cast_fold_t WHERE s IS NOT NULL)
+        """
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to