Skip to content

[fix](nereids) Fold a VARBINARY to string cast to its bytes like BE does - #69025

Merged
morningman merged 3 commits into
apache:masterfrom
morningman:b2m-t35g-varbinary-cast-fold
Oct 16, 2026
Merged

morningman merged 3 commits into
apache:masterfrom
morningman:b2m-t35g-varbinary-cast-fold

Conversation

@morningman

@morningman morningman commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

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. [fix](be) Skip condition cache for complex object literals #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 [fix](be) Skip condition cache for complex object literals #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))).

Release note

Fix wrong results of constant expressions that cast a VARBINARY value to a string, including hex() and to_base64() of a VARBINARY constant.

Check List (For Author)

  • Test

    • Regression test
      • query_p0/sql_functions/binary_functions/test_varbinary_cast_fold: hex(), to_base64(), length() and casts of VARBINARY constants (valid UTF-8, multi-byte, X'FF', empty, NUL, U+1F600), the same casts in scan filters and in nullable IN / NOT IN subqueries, each run with FE folding, with FE and BE folding, and without folding, with the condition cache on; the results of each block match. .out generated by the script.
      • query_p0/sql_functions/binary_functions/test_binary_hex_function: .out regenerated by the script (sql_25).
    • Unit Test
      • FE: FoldConstantTest.testCastVarBinaryToStringFold (folds to the bytes for ASCII and multi-byte UTF-8, leaves X'FF', NUL and U+1F600 unfolded), FoldConstantRuleOnBETest.testSkipCastFromVarBinary, VarBinarySqlSupportTest.testJoinConditionComparingStringsCastFromBinary.
    • Manual test (add detailed scripts or steps below)
    • No need to test or manual test. Explain why:
      • This is a refactor/code format and no logic has been changed.
      • Previous test can cover this change.
      • No code files have been changed.
      • Other reason
  • Behavior changed:

  • Does this need documentation?

    • No.
    • Yes.

Check List (For Reviewer who merge this PR)

  • Confirm the release note
  • Confirm test cases

@hello-stephen

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

@morningman

Copy link
Copy Markdown
Contributor Author

run buildall

@morningman

Copy link
Copy Markdown
Contributor Author

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes: the ordinary ASCII/BMP correction is focused, but three new constant-folding regressions can change predicate results. All are explained inline: [P1] decoded NUL comparison, [P2] bounded VARCHAR supplementary-character corruption, and [P2] trim_in splitting decoded characters. There were no existing inline threads or applicable existing blocking comment IDs.

Critical checkpoint conclusions:

  1. Goal and proof: FE binary-to-string casts should retain BE bytes. The new unit and paired regression tests cover ordinary valid UTF-8 and invalid-byte fallback, but the three findings disprove complete equivalence.
  2. Scope and clarity: one literal implementation and its unit/regression coverage; the malformed-input fallback is documented clearly.
  3. Concurrency and locks: each conversion uses a local decoder and ByteBuffer; no shared decoder, new thread, lock, heavy locked operation or lock-order change.
  4. Lifecycle/static initialization: temporary Java objects only; no persistent resources, cycles, release duties or new static-order dependency.
  5. Configuration: none added. Tests use the existing session folding switch; suite connections are owned and closed by their context.
  6. Compatibility: no wire, storage-format or function-symbol change. Existing STRING_LITERAL/VARBINARY_LITERAL translation is reused; the intended semantic correction has the three regressions above.
  7. Thrift/protobuf: no definition changes, so release-branch field-ID checks do not apply.
  8. Parallel paths: STRING/CHAR/VARCHAR, CAST/TRY_CAST, strict/non-strict and nested casts, direct literal callers, legacy translation and BE execution were traced. Ordinary malformed UTF-8 remains a BE cast rather than becoming NULL; direct FE-only conversions cannot represent arbitrary malformed bytes.
  9. Conditional/error handling: REPORT rejects concrete malformed UTF-8 without replacement; a general AnalysisException deliberately reaches the visitor's defer-to-BE catch rather than its CastException-to-NULL branch. No additional error-handling defect was verified.
  10. Test coverage: JUnit 5 value/fallback checks and ordered fold-on/off regression queries are present. Embedded/trailing NUL comparisons, bounded supplementary characters and trim_in with distinct supplementary characters need coverage for the reported bugs. No tests were executed.
  11. New expected results: all supplied hex/base64/cast/length rows are statically consistent with their SQL, including FF and empty bytes. Neither successful execution nor output-generation provenance was independently verified.
  12. Observability: this local folding change introduces no distributed operation requiring new metrics/logs; no additional observability issue found.
  13. Transactions/persistence: no transaction, visible-version, delete-bitmap, EditLog, replay or failover changes.
  14. Writes/atomicity/crash behavior: no write, commit or recovery mechanism added; no new atomicity, leak or deadlock path verified.
  15. FE-BE variables: none added; existing literal transmission carries actual Java string values and binary buffers.
  16. Performance/memory: linear UTF-8 decoding replaces linear hex encoding. ByteBuffer wraps the existing bytes and local Java allocations have bounded lifetimes; no substantiated performance or BE memory-accounting regression.
  17. Other checks: reviewed caller coercion, null/const/nested cases, Unicode boundaries, comparison rewrites, wire byte lengths, source-based view SQL and test-session isolation. Quoting/view and session-leak suspicions were dismissed with concrete code paths; duplicate NUL reports were merged.

User focus: no additional focus was supplied. All four authoritative changed paths, relevant upstream/downstream code, tests and goldens were reviewed. This was static-only: no builds, tests or source modifications; author-reported runs are not review execution evidence. There are no unresolved candidates or uncovered static scope. Convergence is assessed separately from correctness: the three verified bugs remain even when the review converges.

Review converged after 3 round(s).

/** 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()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Preserve BE comparison semantics for decoded NUL bytes

SELECT CAST(X'610062' AS STRING) = 'a' now folds to TRUE. visitCast creates the string containing a, NUL, b, then visitEqualTo calls StringLikeLiteral.compareTo, which returns equality when the first unmatched byte is zero, even with significant bytes after it. BE preserves all three bytes and compares their explicit lengths, returning FALSE; the previous hex-text fold also returned FALSE.

A reduced triggering plan is Filter(EqualTo(Cast(X'610062', STRING), 'a')) -> Scan(t). visitCast followed by visitEqualTo rewrites its predicate to TRUE, admitting rows BE would reject. Defer NUL-bearing casts until FE comparisons match BE, or correct that comparator alongside this change, and add folded/unfolded comparison coverage.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0c522c3 by leaving such casts to BE, not by changing how FE evaluates strings.

The FE behavior is not specific to binary input. With a plain string literal, SELECT 'a\0b' = 'a' also folds to true, because StringLikeLiteral.compareTo takes the bytes after a NUL for padding, while BE compares all three bytes. Before this PR the binary form folded to the hex text 610062, which happened to give BE's answer. VarBinaryLiteral.castValueToString() now throws for decoded text with a NUL byte or a supplementary character, as it already does for invalid UTF-8, so FoldConstantRuleOnFE.visitCast leaves the cast to BE. Ordinary text still folds.

FoldConstantTest.testCastVarBinaryToStringFold asserts that the cast of X'610062' stays unfolded. test_varbinary_cast_fold has edge_fold / edge_no_fold blocks with this comparison and the two expressions from the other threads; both give false, F09F9880, F09F9880. With folding on, the same expressions gave true, 3F, 3F before this commit.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both forms in the latest review, checked on a local cluster with this PR:

  • With enable_fold_constant_by_be=true, SELECT CAST(X'610062' AS STRING) = 'a' returns false. FoldConstantRuleOnBE matches only the root expression, so BE evaluates the whole comparison; it does not fold the cast alone and leave the comparison to FE. Where BE did fold the cast alone, inside a predicate that is not constant, the cast is now left to BE as well (see the U+FFFD thread).
  • SELECT CAST(X'61' AS STRING) = 'a\0b' does fold to true while BE returns false. The NUL is in the other operand, a plain string literal, and SELECT 'a' = 'a\0b' folds to true on master as well, because StringLikeLiteral.compareTo treats the bytes after a NUL as padding. Before this PR the hex text 61 happened to give BE's answer. This PR does not change how FE compares string literals.

@Override
protected String castValueToString() {
return toString();
return toUtf8String().orElseThrow(() -> new AnalysisException(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Keep supplementary characters intact in bounded VARCHAR folds

SELECT CAST(X'F09F9880' AS VARCHAR(1)) = '?' now folds to TRUE. The decoder returns the intact emoji, but Literal.uncheckedCastToString constructs VarcharLiteral(value, 1), whose UTF-16 substring(0, 1) retains only its high surrogate. The parser produces Project(EqualTo(substr(Cast(X'F09F9880', VARCHAR(1)), 1, 1), '?')); the outer character-count substr cannot restore the discarded half, and StringLikeLiteral.compareTo encodes the lone surrogate as ? (3F). BE retains the whole character before substring and returns FALSE, as did the old hex-text predicate ('F' = '?').

Defer this bounded cast or construct its literal without splitting code points, and cover a supplementary character at the length boundary with folding enabled and disabled.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0c522c3, together with the NUL comparison (see that thread): a cast whose decoded text has a supplementary character is now left to BE. The truncation is not specific to binary input either: CAST('😀' AS VARCHAR(1)) with a plain literal folds to the lone high surrogate, which encodes as 3F, while BE keeps F09F9880. FoldConstantTest asserts that CAST(X'F09F9880' AS VARCHAR(1)) stays unfolded, and test_varbinary_cast_fold checks hex(CAST(X'F09F9880' AS VARCHAR(1))) = F09F9880 with folding on and off.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With enable_fold_constant_by_be=true, a constant expression is still evaluated as a whole by BE, so hex(CAST(CAST(X'F09F9880' AS STRING) AS VARCHAR(1))) stays F09F9880. Inside a filter, WHERE s = CAST(CAST(X'F09F9880' AS STRING) AS VARCHAR(1)) was rewritten to s = '?', because BE folded the inner part and FE cut the result to one UTF-16 unit. This is fixed in 6b6e19d by not folding casts from VARBINARY on BE, and the filter block of test_varbinary_cast_fold covers it in all three modes.

return Optional.of(StandardCharsets.UTF_8.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT)
.decode(ByteBuffer.wrap(byteValues)).toString());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Keep decoded characters whole during trim_in folding

SELECT trim_in(CAST(X'F09F9880' AS STRING), '😁') = '?' newly folds to TRUE. The decoded 😀 and trim-set 😁 share a UTF-16 high surrogate. StringArithmetic.trimInImpl checks membership with charAt/indexOf, removes that matching half and leaves an unmatched low surrogate; UTF-8 comparison turns it into ?. BE TrimInUtil compares whole UTF-8 characters, leaves 😀 intact and returns FALSE, as did the old hex-text predicate.

The reduced Project(EqualTo(trim_in(Cast(binary, STRING), '😁'), '?')) becomes Project(TRUE). This uses an unbounded STRING cast, so guarding bounded VARCHAR targets alone does not fix it. Use code-point membership in the trim-set implementation or defer affected folding, and add folded/unfolded coverage for trim_in and its shared ltrim_in/rtrim_in paths.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0c522c3: the cast of X'F09F9880' is now left to BE, so trim_in over it is no longer folded either. StringArithmetic.trimInImpl does split surrogate pairs, and a plain literal reaches it too: trim_in('😀', '😁') folds to 3F while BE returns F09F9880. That trim-set code is shared with ltrim_in / rtrim_in and is unchanged here. test_varbinary_cast_fold checks hex(trim_in(CAST(X'F09F9880' AS STRING), CAST(X'F09F9881' AS STRING))) = F09F9880 with folding on and off.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With enable_fold_constant_by_be=true, BE evaluates the whole constant trim_in(...), so the edge block of test_varbinary_cast_fold gives F09F9880 for hex(trim_in(CAST(X'F09F9880' AS STRING), CAST(X'F09F9881' AS STRING))) in all three modes. A cast from VARBINARY inside a larger expression is no longer folded on BE (6b6e19d), so FE never sees the decoded string there either.

@morningman

Copy link
Copy Markdown
Contributor Author

run buildall

@morningman

Copy link
Copy Markdown
Contributor Author

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes. Three new P2 findings cover invalid UTF-8 losing its byte identity during optional BE constant folding, a legal nullable IN subquery rejected after deferral, and incorrect nested bounded-array conversion. The existing P1 NUL-comparison finding also remains applicable; it is retained without a duplicate inline comment. All four changed files were reviewed at head 0c522c34565490355e0ec132e9161114d3f667c9.

The original direct FE reproducers are guarded, but the complete folding pipeline still has these known residuals:

  • NUL comparison, 4258348115: with enable_fold_constant_by_be=true, the original CAST(X'610062' AS STRING) = 'a' again folds true after backend materialization bypasses the guard. With the default FE-only mode, CAST(X'61' AS STRING) = 'a\0b' also folds true because the opposite operand contains NUL. BE returns false for both. This existing P1 is carried in existing_blocking_comment_ids.
  • Bounded supplementary truncation, 4258348122: the original single bounded binary cast is repaired. With optional BE folding, CAST(CAST(X'F09F9880' AS STRING) AS VARCHAR(1)) = '?' reaches the same known UTF-16 split through a subsequent cast. No duplicate inline is added.
  • Supplementary trim-set folding, 4258348131: the original predicate remains wrong with optional BE folding, which recreates the supplementary string outside the guard before trim_in folds. Shared ltrim_in/rtrim_in use the same mechanism. No duplicate inline is added.

The required review checkpoint conclusions are:

  • Goal and proof: safe UTF-8 decoding fixes the scalar hex-text conversion, but parent evaluation, optional backend materialization, nested bounded casts, and mark-join checks still prevent complete correctness. Added tests support the direct paths; no tests were executed here.
  • Scope and clarity: one focused literal change with relevant FE unit and regression coverage; comments explain the special guards.
  • Concurrency and locks: no new shared mutable state, threads, locks, or lock-order change. Decoder and buffers are per-call locals.
  • Lifecycle and initialization: synchronous local resources; no new ownership transfer, static initialization dependency, or release path.
  • Configuration: none added. Existing strict, TRY_CAST, skip-fold, and optional BE-fold modes were traced; the enabled BE-fold mode exposes the new finding.
  • Symbols, formats, and rolling compatibility: no symbol, stored format, or protocol definition changes. Existing backend result decoding has the newly reachable limitation reported inline.
  • Thrift/protobuf IDs: no changed definitions; a cross-branch ID audit is inapplicable.
  • Parallel paths: explicit/implicit casts, strict/TRY_CAST, scalar and recursive complex literal casts, nested normalization, direct caller domains, legacy/raw serialization, downstream consumers, hash/mark joins, and backend constant replacement were covered.
  • Special conditions and errors: invalid UTF-8/NUL/supplementary deferral has concrete justification and avoids NULL folding; deferral alone does not protect the complete pipeline.
  • Test coverage: ASCII, BMP, empty, invalid UTF-8, NUL, and supplementary values have scalar unit/regression sources. A deterministic backend-fold on/off matrix, nullable constant-IN subqueries, nested bounded-array casts, and known residuals need coverage.
  • Expected outputs: all eight blocks match inspected semantics with backend folding disabled; cache aliases and normalization make the paired paths meaningful. Fuzzy P0 can enable backend folding, so the suite does not establish correctness across that mode.
  • Observability: no new distributed operation needing additional logging or metrics; existing fold deferral/error handling remains available.
  • Transactions and persistence: no transaction, EditLog, failover, or persistence path changed.
  • Data writes and crash atomicity: no data-writing or storage lifecycle change; version visibility and MoW bitmap invariants are unaffected.
  • FE/BE variables: none added; admitted strings and deferred binary inputs use existing send paths.
  • Performance and memory: linear decoding/validation and local allocations; no substantiated additional performance or ownership defect.
  • Other issues: no additional distinct verified defect after the complete static scope and duplicate review.

No additional user focus was supplied. Validation was static only: no builds, unit/regression tests, or style commands ran, and generated-golden provenance is not independently proven by the checkout. Runtime results described above are inferred from inspected control flow and the pinned protobuf implementation.

Review converged after three rounds: all three final reviewers returned NO_NEW_VALUABLE_FINDINGS, full changed-file and related-path coverage is complete, and every candidate/risk was verified, accepted, deduplicated, or dismissed with concrete evidence. No unresolved static review question remains; the execution and test-source coverage limits above remain disclosed.

Review converged after 3 round(s).

Existing P0/P1 findings confirmed for this head: #69025 (comment)

@Override
protected String castValueToString() {
return toString();
String value = toUtf8String().orElseThrow(() -> new AnalysisException(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve invalid bytes through optional BE constant folding

With enable_fold_constant_by_be=true, SELECT CAST(X'FF' AS STRING) = '�' is newly folded to TRUE; full BE evaluation returns FALSE. The right operand is U+FFFD (EFBFBD), distinct from the input byte FF.

A reduced triggering plan is:

Filter(EqualTo(Cast(X'FF', STRING), '�'))
  Scan(t)

visitCast catches this AnalysisException and retains the cast, but the bottom-up FoldConstantRule then applies FoldConstantRuleOnBE to that child. BE returns raw FF through PValues.string_value; getResultExpression creates StringLiteral(getStringValue(i)), and the protobuf Java getter substitutes U+FFFD for invalid UTF-8. Parent visitEqualTo consequently produces Filter(TRUE), admitting rows BE would reject. The previous hex-text fold compared 'FF' with U+FFFD and returned FALSE.

Keep unrepresentable casts out of BE constant replacement as well, or validate the raw returned bytes before constructing an FE string literal. Add predicate and byte-visible coverage with enable_fold_constant_by_be both on and off; the new FE-only unit test does not exercise this result transport.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed where the cast is not the whole constant expression, and fixed in 6b6e19d. With enable_fold_constant_by_be=true, WHERE s = CAST(X'FF' AS STRING) was rewritten to s = '�': the cast is the largest constant part of the predicate, BE folded it, and getResultExpression decoded the returned byte 0xFF as U+FFFD, so the filter matched a U+FFFD row instead of the 0xFF row. FoldConstantRuleOnBE.shouldSkipFold now skips a cast from a type that contains VARBINARY, as it already skips from_base64 for this reason, so BE evaluates the cast when it runs the query.

The constant form in this comment, SELECT CAST(X'FF' AS STRING) = '�', returns false on a local cluster: the BE rule matches only the root expression (root(Expression.class)), so BE evaluated the whole comparison. test_varbinary_cast_fold now runs every block with FE folding, with FE and BE folding, and without folding; its filter and in blocks cover this case.

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)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Allow deferred string constants in nullable IN subqueries

With the default enable_fold_constant_by_be=false, SELECT CAST(X'610062' AS STRING) IN (SELECT s FROM t2) now fails with varbinary type could not in join equal conditions when t2.s is nullable STRING. The comparison operands are both STRING.

The reduced rewritten plan is:

Project(mark)
  MarkJoin(mark: EqualTo(Cast(X'610062', STRING), t2.s))
    OneRowRelation(dummy)
    Scan(t2: s nullable STRING)

This new guard retains the constant cast. SubqueryToApply keeps it as the compare expression, and InApplyToJoin preserves the nullable equality as a mark conjunct. JoinUtils.isHashJoinCondition rejects the slot-free constant operand, so PushDownExpressionsInHashCondition leaves this non-hash mark conjunct unchanged; the nested-loop projection rule also excludes mark joins. CheckAfterRewrite then rejects the binary literal descendant inside the STRING cast. Previously, folding to hex text removed that descendant and the query passed this check.

Keep supported deferred STRING operands valid at this planner boundary, by checking the actual compared types or projecting the constant expression to a STRING slot, and cover a constant cast in a nullable IN subquery's SELECT result.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed, and fixed in 6b6e19d. CheckAfterRewrite rejected a hash or mark join condition that had any VARBINARY sub-expression, so CAST(X'610062' AS STRING) IN (SELECT s FROM t) failed although the condition compares two strings. It now rejects a condition only when a comparison in it has a VARBINARY operand, which is what BE cannot hash or compare. VarBinarySqlSupportTest.testJoinConditionComparingStringsCastFromBinary plans IN and NOT IN subqueries over such a cast and checks that comparing two VARBINARY slots is still rejected in hash and mark join conditions. The in block of test_varbinary_cast_fold returns the same results in all three folding modes.

throw new AnalysisException("Cannot cast VARBINARY " + computeToSql()
+ " to a string type on FE: it has a NUL byte or a supplementary character");
}
return value;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Match BE semantics for bounded strings inside array casts

SELECT CAST([X'6162'] AS ARRAY<VARCHAR(1)>)[1] = 'a' now returns TRUE with FE folding, whereas skipping folding returns FALSE. This uses the two ASCII bytes ab, so the NUL/supplementary guards do not apply.

In the reduced Project(ElementAt(Cast([X'6162'], ARRAY<VARCHAR(1)>), 1) = 'a'), ArrayLiteral.uncheckedCastTo calls the element's checked cast. This method returns ab, and Literal.uncheckedCastToString builds VarcharLiteral("ab", 1), replacing the array cast with ['a']. On BE, create_array_wrapper recursively invokes the string cast wrapper, which preserves both bytes and does not enforce the nested VARCHAR length. The parser adds substr only when the whole cast target is string-like, so no nested substring narrows ['ab']; extracting its element and comparing with a returns false. The previous hex-text fold truncated 6162 to 6, so this predicate previously also returned false.

Defer these bounded recursive conversions or make FE element folding agree with BE, and add folded/unfolded bounded-array coverage. This is a nested length-semantics mismatch, distinct from the existing supplementary-character splitting report.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changed here. The difference is not specific to binary input. With a plain string array, SELECT CAST(['ab'] AS ARRAY<VARCHAR(1)>)[1] = 'a' also folds to true on FE, because the array literal truncates each element to the target length, while BE returns false. This happens on master and with this PR. Before this PR, CAST([X'6162'] AS ARRAY<VARCHAR(1)>) folded to ['6'], the truncated hex text, so the comparison with 'a' gave BE's answer only because the folded value was wrong. Making FE fold nested VARCHAR(n) casts like BE is a separate change to how array literals are cast.

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 28995 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit 0c522c34565490355e0ec132e9161114d3f667c9, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17601	5620	5531	5531
q2	2140	291	249	249
q3	10275	1381	820	820
q4	4678	473	358	358
q5	7423	771	545	545
q6	181	186	158	158
q7	755	795	598	598
q8	9242	1354	1087	1087
q9	5674	4421	4422	4421
q10	6813	1302	1017	1017
q11	429	252	233	233
q12	627	402	289	289
q13	18067	3076	2371	2371
q14	279	269	240	240
q15	q16	733	719	672	672
q17	1167	756	571	571
q18	7060	6199	6165	6165
q19	1093	945	604	604
q20	366	338	226	226
q21	5513	2555	2549	2549
q22	394	346	291	291
Total cold run time: 100510 ms
Total hot run time: 28995 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	6484	6475	6454	6454
q2	714	604	571	571
q3	5560	5692	5231	5231
q4	2042	2199	1819	1819
q5	5779	5710	5670	5670
q6	244	195	160	160
q7	2321	2031	1779	1779
q8	3131	2712	2720	2712
q9	8420	8007	7802	7802
q10	3625	3574	3224	3224
q11	613	414	376	376
q12	664	697	501	501
q13	2759	3075	2378	2378
q14	290	290	276	276
q15	q16	679	686	620	620
q17	7839	6991	6876	6876
q18	13158	12233	13123	12233
q19	913	784	797	784
q20	2189	2137	1963	1963
q21	5774	4684	4780	4684
q22	524	441	415	415
Total cold run time: 73722 ms
Total hot run time: 66528 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 151603 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit 0c522c34565490355e0ec132e9161114d3f667c9, data reload: false

query5	4311	589	453	453
query6	414	214	205	205
query7	4795	439	223	223
query8	317	179	172	172
query9	8767	3977	3937	3937
query10	471	312	258	258
query11	5923	3408	3175	3175
query12	144	86	85	85
query13	1240	427	316	316
query14	6553	4803	4492	4492
query14_1	4288	4245	4208	4208
query15	200	208	181	181
query16	938	473	416	416
query17	860	656	545	545
query18	2417	415	314	314
query19	200	159	133	133
query20	81	79	79	79
query21	216	135	111	111
query22	12945	13072	12730	12730
query23	13078	12380	12096	12096
query23_1	12108	12192	12223	12192
query24	7100	836	458	458
query24_1	465	459	482	459
query25	509	392	332	332
query26	1242	248	134	134
query27	2780	437	261	261
query28	4648	1938	1943	1938
query29	1530	573	425	425
query30	293	215	179	179
query31	872	755	647	647
query32	130	87	88	87
query33	503	291	237	237
query34	960	854	498	498
query35	746	757	628	628
query36	833	806	733	733
query37	126	95	92	92
query38	1789	1754	1705	1705
query39	700	695	667	667
query39_1	652	677	669	669
query40	213	114	95	95
query41	65	66	64	64
query42	86	84	80	80
query43	370	369	331	331
query44	1319	722	714	714
query45	178	173	164	164
query46	847	970	561	561
query47	3008	3039	2972	2972
query48	292	311	216	216
query49	568	387	306	306
query50	677	284	205	205
query51	10562	10789	10294	10294
query52	78	79	70	70
query53	183	208	147	147
query54	231	213	175	175
query55	70	65	62	62
query56	220	212	215	212
query57	1616	1594	1575	1575
query58	280	258	253	253
query59	2354	2379	2137	2137
query60	270	230	213	213
query61	138	152	133	133
query62	389	335	287	287
query63	194	161	162	161
query64	2730	905	757	757
query65	3448	3383	3384	3383
query66	1776	407	297	297
query67	20440	20566	20137	20137
query68	3074	988	626	626
query69	381	287	246	246
query70	926	832	811	811
query71	283	236	205	205
query72	2685	2521	2233	2233
query73	523	533	293	293
query74	4567	4481	4317	4317
query75	2306	2293	1912	1912
query76	2288	1019	652	652
query77	363	382	299	299
query78	9029	8926	8507	8507
query79	898	845	512	512
query80	467	425	334	334
query81	509	332	272	272
query82	207	136	115	115
query83	191	194	180	180
query84	285	120	90	90
query85	759	429	349	349
query86	254	237	239	237
query87	2027	1950	1832	1832
query88	3541	2693	2666	2666
query89	328	294	252	252
query90	2082	174	182	174
query91	158	152	123	123
query92	95	87	84	84
query93	972	993	579	579
query94	445	304	271	271
query95	581	345	316	316
query96	659	525	235	235
query97	2432	2443	2294	2294
query98	156	147	140	140
query99	764	771	659	659
Total cold run time: 232762 ms
Total hot run time: 151603 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 24.53 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit 0c522c34565490355e0ec132e9161114d3f667c9, data reload: false

query1	0.01	0.01	0.00
query2	0.09	0.05	0.05
query3	0.26	0.12	0.12
query4	1.60	0.12	0.12
query5	0.28	0.25	0.25
query6	1.17	0.68	0.68
query7	0.04	0.01	0.00
query8	0.05	0.04	0.04
query9	0.42	0.35	0.35
query10	0.57	0.58	0.60
query11	0.20	0.14	0.14
query12	0.19	0.15	0.14
query13	0.49	0.48	0.49
query14	0.98	0.97	0.95
query15	0.63	0.61	0.60
query16	0.33	0.34	0.35
query17	1.15	1.19	1.15
query18	0.23	0.21	0.22
query19	2.10	1.97	1.99
query20	0.02	0.02	0.01
query21	15.47	0.25	0.14
query22	4.48	0.05	0.05
query23	16.25	0.33	0.13
query24	2.36	1.58	0.45
query25	0.12	0.06	0.06
query26	1.58	0.24	0.14
query27	0.04	0.04	0.05
query28	1.80	0.60	0.41
query29	12.55	4.16	3.27
query30	0.28	0.15	0.13
query31	2.77	0.60	0.34
query32	3.22	0.62	0.52
query33	3.25	3.32	3.17
query34	15.36	4.02	3.37
query35	3.36	3.37	3.36
query36	0.53	0.45	0.43
query37	0.09	0.07	0.06
query38	0.05	0.03	0.04
query39	0.04	0.04	0.03
query40	0.18	0.14	0.15
query41	0.08	0.02	0.02
query42	0.04	0.03	0.03
query43	0.04	0.03	0.04
Total cold run time: 94.75 s
Total hot run time: 24.53 s

@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage 100.00% (11/11) 🎉
Increment coverage report
Complete coverage report

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 100.00% (11/11) 🎉
Increment coverage report
Complete coverage report

@morningman

Copy link
Copy Markdown
Contributor Author

run buildall

@morningman

Copy link
Copy Markdown
Contributor Author

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Static review of all 12 changed files at 6b6e19d020cca68c4156d25445bdef71b7ca3aa6: no additional independent inline findings. All three assigned reviewers returned NO_NEW_VALUABLE_FINDINGS; all candidates and initial risks were verified, dismissed with evidence, or deduplicated. The review converged in one round.

The inspected chain is binary literal -> FE checked string cast -> UTF-8 literal or deferred Cast -> optional BE constant collection -> join validation/expression translation -> BE byte-preserving evaluation. A retained binary literal propagates digest zero through its expression and scan, disabling condition-cache lookup/insertion.

Existing review context: the original P1 NUL-bearing cast in 4258348115 is fixed by FE deferral and the deep BE-folding skip; no existing original P0/P1 finding remains applicable. The invalid-byte BE-folding, supplementary-character, trim_in and nullable-IN reports are addressed by the inspected guards/validator. The bounded ARRAY<VARCHAR(n)> mismatch in 4260479286 still applies and was not reposted. The opposite-operand plain-string NUL case discussed in the P1 thread's reply also remains known context.

Individual critical-checkpoint conclusions:

  1. Goal and evidence: representable binary-to-text constants now carry their UTF-8 bytes instead of hex text; unsafe casts remain for BE. Added unit/regression test sources cover the primary reproductions, with the existing bounded-array limitation above.
  2. Focus and reuse: changes remain focused on folding, its join/cache consequences and tests; recursive FE type detection reuses the existing helper.
  3. Concurrency and locks: no new production shared mutation, threads or locks; decoder state is local and BE type traversal is read-only. Existing lock order is unaffected.
  4. Lifecycle and static initialization: no new owners, callbacks, cycles or cross-TU static dependencies; existing literal column ownership is retained.
  5. Configuration: no new option; tests exercise existing FE/BE folding and cache settings on suite-owned connections.
  6. Compatibility: no wire, storage-format or function-symbol layout change. Existing Cast execution and cache-disabled sentinel remain in use; no new libc API is introduced.
  7. Thrift/protobuf IDs: no gensrc definitions changed.
  8. Parallel paths: scalar string types, TryCast, array/map/struct recursion, outer wrappers, optional BE folding, hash/mark joins and cache consumers were traced. Whole-subtree collection checks prevent unsafe casts being folded through wrappers. Complex join keys use implemented serialization/comparison; standalone marks broadcast and avoid partitioned spill.
  9. Special conditions and errors: concrete UTF-8/NUL/supplementary comments explain deferral. FE representation failures retain casts rather than create NULL; direct VARBINARY comparison restrictions remain enforced.
  10. Tests: source coverage includes ASCII/BMP text, invalid UTF-8, empty bytes, NUL, supplementary text, scan filters, ARRAY and nullable IN/NOT IN in three folding modes. Planner tests retain negative direct-binary hash/mark checks. Shared map/struct, CHAR and external-scan paths were inspected without added dedicated runtime cases.
  11. Changed results: all new golden blocks and the changed old sql_25 match the SQL's byte values and mode settings under static inspection. Ordering and table setup follow regression conventions. Generation provenance was not verified.
  12. Observability: existing analysis errors, folding diagnostics and cache counters remain sufficient for these paths; no new distributed operation needs metrics.
  13. Transactions/persistence: no EditLog, visible-version, delete-bitmap or transaction lifecycle changes.
  14. Writes/atomicity: no production write or crash-cleanup mechanism is modified; existing insert evaluation retains the byte semantics.
  15. FE/BE variables: no new transmitted variable or type; typed Cast/literal translation uses the existing protocol.
  16. Performance/memory: decoding and type/tree traversal are linear and short-circuit where applicable; BE adds no per-row copying or new owned buffer. Cache bypass is the intended correctness fallback.
  17. Other issues: the final missed-path sweep found no additional substantiated issue; no changed file or correctness question remains unreviewed.

User focus: no additional focus was supplied. Validation limits: this invocation performed static inspection only. No builds, tests, formatting/hygiene scripts or ELF compatibility checks were run; inspected tests, goldens and author reports are not executed test evidence.

Review converged after 1 round(s).

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 29109 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit 0c522c34565490355e0ec132e9161114d3f667c9, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17574	5642	5523	5523
q2	2087	305	275	275
q3	10245	1382	848	848
q4	4676	474	345	345
q5	7443	803	536	536
q6	188	187	157	157
q7	767	804	607	607
q8	9246	1341	1083	1083
q9	5730	4485	4433	4433
q10	6837	1285	1026	1026
q11	439	267	237	237
q12	630	417	285	285
q13	18038	3086	2389	2389
q14	281	268	248	248
q15	q16	730	718	664	664
q17	1170	736	579	579
q18	7112	6238	6185	6185
q19	1100	941	608	608
q20	400	343	219	219
q21	5449	2580	2569	2569
q22	394	357	293	293
Total cold run time: 100536 ms
Total hot run time: 29109 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	6560	6526	6528	6526
q2	720	585	572	572
q3	5537	5666	5257	5257
q4	2065	2153	1565	1565
q5	5964	5716	5688	5688
q6	263	193	153	153
q7	2367	2087	1722	1722
q8	3163	2757	2733	2733
q9	8315	8243	7749	7749
q10	3670	3592	3251	3251
q11	605	407	376	376
q12	667	696	498	498
q13	2759	3104	2360	2360
q14	297	285	256	256
q15	q16	671	690	619	619
q17	7864	6966	6848	6848
q18	13139	12308	13331	12308
q19	976	881	867	867
q20	2231	2231	1968	1968
q21	6182	4815	4781	4781
q22	509	463	412	412
Total cold run time: 74524 ms
Total hot run time: 66509 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 152470 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit 0c522c34565490355e0ec132e9161114d3f667c9, data reload: false

query5	4310	592	443	443
query6	424	200	184	184
query7	4793	432	225	225
query8	322	180	168	168
query9	8802	3958	3925	3925
query10	469	323	273	273
query11	5996	3409	3200	3200
query12	155	90	82	82
query13	1237	420	300	300
query14	6558	4735	4507	4507
query14_1	4294	4281	4251	4251
query15	210	199	187	187
query16	937	442	425	425
query17	896	664	550	550
query18	2410	420	311	311
query19	202	161	141	141
query20	84	82	80	80
query21	210	136	112	112
query22	12951	13003	12773	12773
query23	13132	12522	12138	12138
query23_1	12248	12318	12350	12318
query24	7128	831	463	463
query24_1	471	471	480	471
query25	544	401	337	337
query26	1248	254	136	136
query27	2763	437	265	265
query28	4613	1929	1927	1927
query29	1529	576	433	433
query30	289	213	180	180
query31	882	770	648	648
query32	131	92	90	90
query33	505	284	239	239
query34	898	849	508	508
query35	751	778	650	650
query36	871	843	740	740
query37	128	100	89	89
query38	1766	1732	1730	1730
query39	707	739	686	686
query39_1	697	683	667	667
query40	220	112	97	97
query41	63	62	62	62
query42	85	83	86	83
query43	364	381	344	344
query44	1329	704	705	704
query45	178	172	158	158
query46	849	961	558	558
query47	3008	3029	2949	2949
query48	299	317	217	217
query49	567	422	297	297
query50	711	277	217	217
query51	10790	10527	10647	10527
query52	77	83	69	69
query53	193	203	156	156
query54	233	217	178	178
query55	69	76	67	67
query56	225	214	201	201
query57	1616	1589	1557	1557
query58	272	253	246	246
query59	2396	2381	2130	2130
query60	269	236	214	214
query61	143	147	145	145
query62	392	340	287	287
query63	187	155	168	155
query64	2711	941	760	760
query65	3432	3340	3374	3340
query66	1775	405	298	298
query67	20246	20061	20174	20061
query68	3006	997	622	622
query69	387	294	242	242
query70	914	828	839	828
query71	296	230	204	204
query72	2630	2609	2498	2498
query73	539	538	310	310
query74	4585	4491	4345	4345
query75	2344	2317	1944	1944
query76	2304	1024	606	606
query77	365	403	300	300
query78	9017	9066	8478	8478
query79	1038	865	530	530
query80	544	439	378	378
query81	526	327	279	279
query82	581	145	124	124
query83	316	202	188	188
query84	310	131	106	106
query85	866	400	354	354
query86	319	240	229	229
query87	2002	1946	1794	1794
query88	3625	2718	2698	2698
query89	340	290	254	254
query90	1893	184	179	179
query91	156	150	123	123
query92	98	87	78	78
query93	1006	1011	575	575
query94	454	317	273	273
query95	587	417	322	322
query96	670	544	235	235
query97	2466	2418	2315	2315
query98	175	155	142	142
query99	792	766	654	654
Total cold run time: 234162 ms
Total hot run time: 152470 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 24.6 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit 0c522c34565490355e0ec132e9161114d3f667c9, data reload: false

query1	0.00	0.00	0.00
query2	0.10	0.05	0.04
query3	0.26	0.12	0.12
query4	1.61	0.13	0.12
query5	0.25	0.26	0.22
query6	1.17	0.68	0.68
query7	0.04	0.00	0.00
query8	0.06	0.03	0.04
query9	0.43	0.36	0.36
query10	0.56	0.58	0.64
query11	0.20	0.15	0.14
query12	0.18	0.14	0.14
query13	0.49	0.48	0.52
query14	0.97	0.97	0.98
query15	0.63	0.61	0.62
query16	0.34	0.35	0.34
query17	1.16	1.21	1.16
query18	0.23	0.21	0.22
query19	2.11	2.00	1.99
query20	0.01	0.01	0.02
query21	15.50	0.23	0.14
query22	4.55	0.06	0.05
query23	16.23	0.32	0.12
query24	3.04	0.60	0.48
query25	0.11	0.06	0.05
query26	1.33	0.27	0.15
query27	0.04	0.04	0.03
query28	2.05	0.61	0.42
query29	12.49	4.20	3.32
query30	0.28	0.14	0.14
query31	2.77	0.59	0.34
query32	3.22	0.61	0.52
query33	3.17	3.18	3.16
query34	15.52	4.06	3.39
query35	3.39	3.36	3.33
query36	0.54	0.45	0.42
query37	0.10	0.06	0.06
query38	0.04	0.04	0.03
query39	0.04	0.03	0.03
query40	0.18	0.15	0.13
query41	0.08	0.04	0.03
query42	0.03	0.03	0.03
query43	0.05	0.03	0.04
Total cold run time: 95.55 s
Total hot run time: 24.6 s

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 29529 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit 6b6e19d020cca68c4156d25445bdef71b7ca3aa6, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17603	5796	5703	5703
q2	2100	298	241	241
q3	10279	1506	889	889
q4	4679	472	351	351
q5	7446	830	547	547
q6	211	211	170	170
q7	835	852	618	618
q8	9261	1525	1228	1228
q9	5918	4666	4633	4633
q10	6901	1304	1041	1041
q11	464	262	252	252
q12	627	445	289	289
q13	18033	3144	2380	2380
q14	274	262	250	250
q15	q16	725	718	687	687
q17	1308	761	606	606
q18	7085	6253	6168	6168
q19	1113	1098	632	632
q20	383	355	230	230
q21	5313	2333	2420	2333
q22	376	327	281	281
Total cold run time: 100934 ms
Total hot run time: 29529 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	6313	6302	6314	6302
q2	730	571	549	549
q3	5222	5408	4875	4875
q4	2099	2201	1562	1562
q5	5265	5708	5723	5708
q6	314	238	163	163
q7	2294	2098	1901	1901
q8	3271	2891	2864	2864
q9	8148	8169	8418	8169
q10	3805	3723	3455	3455
q11	667	481	444	444
q12	760	781	542	542
q13	3043	3311	2556	2556
q14	311	312	278	278
q15	q16	699	765	663	663
q17	8684	7612	7659	7612
q18	13202	12304	13078	12304
q19	1057	840	858	840
q20	2186	2177	1943	1943
q21	6084	4893	4994	4893
q22	533	452	417	417
Total cold run time: 74687 ms
Total hot run time: 68040 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 152795 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit 6b6e19d020cca68c4156d25445bdef71b7ca3aa6, data reload: false

query5	4312	618	458	458
query6	422	211	192	192
query7	4810	437	227	227
query8	330	189	172	172
query9	8742	4020	4017	4017
query10	475	312	260	260
query11	5906	3403	3178	3178
query12	148	88	85	85
query13	1282	433	329	329
query14	6522	4749	4550	4550
query14_1	4249	4288	4244	4244
query15	207	204	186	186
query16	949	449	379	379
query17	859	653	548	548
query18	2442	423	318	318
query19	201	173	136	136
query20	84	81	81	81
query21	209	137	111	111
query22	13016	13243	12880	12880
query23	13086	12478	12186	12186
query23_1	12321	12147	12146	12146
query24	7084	821	466	466
query24_1	478	490	477	477
query25	541	424	377	377
query26	1310	261	143	143
query27	2748	442	272	272
query28	4602	1943	1952	1943
query29	1566	607	471	471
query30	301	219	190	190
query31	895	769	651	651
query32	138	94	93	93
query33	515	312	251	251
query34	909	842	511	511
query35	752	783	652	652
query36	828	819	733	733
query37	129	105	91	91
query38	1806	1737	1725	1725
query39	727	704	670	670
query39_1	677	664	677	664
query40	227	119	101	101
query41	73	71	73	71
query42	95	94	83	83
query43	386	389	333	333
query44	1318	714	722	714
query45	183	178	171	171
query46	853	947	560	560
query47	3026	3040	2943	2943
query48	290	305	224	224
query49	581	391	290	290
query50	685	273	228	228
query51	10736	10665	10706	10665
query52	79	87	69	69
query53	183	212	150	150
query54	232	184	188	184
query55	80	68	66	66
query56	228	215	218	215
query57	1627	1586	1586	1586
query58	279	261	246	246
query59	2376	2388	2115	2115
query60	293	238	243	238
query61	153	151	148	148
query62	389	339	285	285
query63	190	165	169	165
query64	2746	941	787	787
query65	3415	3373	3386	3373
query66	1777	416	302	302
query67	20357	20131	20043	20043
query68	2869	992	613	613
query69	391	307	253	253
query70	924	858	831	831
query71	290	230	205	205
query72	2672	2574	2334	2334
query73	512	518	290	290
query74	4596	4467	4325	4325
query75	2341	2310	1955	1955
query76	2310	1014	629	629
query77	356	397	302	302
query78	9147	8947	8418	8418
query79	991	857	530	530
query80	930	458	338	338
query81	574	321	282	282
query82	575	132	112	112
query83	312	197	179	179
query84	307	132	109	109
query85	857	426	405	405
query86	341	239	229	229
query87	2045	1976	1831	1831
query88	3610	2712	2701	2701
query89	341	295	261	261
query90	1792	187	192	187
query91	158	145	126	126
query92	96	87	87	87
query93	1005	989	596	596
query94	529	318	279	279
query95	593	418	324	324
query96	670	517	238	238
query97	2443	2419	2299	2299
query98	170	147	142	142
query99	780	763	642	642
Total cold run time: 234701 ms
Total hot run time: 152795 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 24.62 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit 6b6e19d020cca68c4156d25445bdef71b7ca3aa6, data reload: false

query1	0.01	0.01	0.01
query2	0.11	0.04	0.05
query3	0.26	0.12	0.12
query4	1.61	0.12	0.12
query5	0.28	0.25	0.25
query6	1.17	0.68	0.69
query7	0.04	0.00	0.00
query8	0.05	0.04	0.04
query9	0.42	0.36	0.36
query10	0.57	0.58	0.58
query11	0.21	0.14	0.14
query12	0.18	0.14	0.14
query13	0.49	0.50	0.50
query14	0.98	0.96	0.97
query15	0.64	0.60	0.61
query16	0.33	0.36	0.36
query17	1.15	1.12	1.10
query18	0.22	0.20	0.21
query19	2.06	2.04	1.99
query20	0.02	0.01	0.01
query21	15.50	0.28	0.14
query22	4.46	0.05	0.05
query23	16.27	0.32	0.12
query24	3.38	0.60	0.34
query25	0.11	0.07	0.06
query26	1.08	0.26	0.15
query27	0.03	0.05	0.05
query28	2.00	0.57	0.40
query29	12.54	4.13	3.31
query30	0.28	0.14	0.16
query31	2.81	0.59	0.33
query32	3.23	0.61	0.52
query33	3.15	3.32	3.30
query34	15.47	4.01	3.42
query35	3.36	3.33	3.36
query36	0.56	0.44	0.41
query37	0.10	0.07	0.06
query38	0.05	0.04	0.03
query39	0.03	0.03	0.03
query40	0.17	0.15	0.15
query41	0.08	0.03	0.03
query42	0.03	0.02	0.03
query43	0.04	0.03	0.03
Total cold run time: 95.53 s
Total hot run time: 24.62 s

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 100% (0/0) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 76.94% (35505/46147)
Line Coverage 62.22% (401999/646133)
Region Coverage 58.59% (339173/578923)
Branch Coverage 59.50% (156219/262548)

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 78.95% (15/19) 🎉
Increment coverage report
Complete coverage report

924060929
924060929 previously approved these changes Oct 14, 2026
yujun777
yujun777 previously approved these changes Oct 15, 2026

@yujun777 yujun777 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

morningman and others added 3 commits October 15, 2026 10:31
### What problem does this PR solve?

Issue Number: None

Related PR: apache#68968

Problem Summary: BE casts a VARBINARY value to a string type by keeping its
bytes: CAST(X'616263' AS STRING) is 'abc'. FE constant folding turned the
same cast into the hex text of the bytes ('616263'), because
VarBinaryLiteral.castValueToString() returned toString(). Functions that only
have string signatures, such as hex() and to_base64(), take a VARBINARY
argument through that cast, so their folded results were wrong:

    SELECT hex(cast('abc' AS varbinary(16)))       -- 363136323633, BE: 616263
    SELECT to_base64(cast('abc' AS varbinary(16))) -- NjE2MjYz,     BE: YWJj
    SELECT hex(X'FF')                              -- 4646,         BE: FF

Fold the cast to the bytes decoded as UTF-8. Bytes that are not valid UTF-8
cannot be carried by an FE string literal; castValueToString() throws an
AnalysisException for them, and FoldConstantRuleOnFE.visitCast, which turns
only a CastException into NULL, leaves such a cast for BE to evaluate.

### Release note

Fix wrong results of constant expressions that cast a VARBINARY value to a
string, including hex() and to_base64() of a VARBINARY constant.

### Check List (For Author)

- Test: Unit Test, Regression test
    - FE UT: FoldConstantTest.testCastVarBinaryToStringFold
    - Regression: query_p0/sql_functions/binary_functions/test_varbinary_cast_fold
      (the same queries with constant folding on and off)
- Behavior changed: Yes. Folded casts of VARBINARY constants to string types
  now return the bytes instead of their hex text, as BE does.
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…ters to BE

Folding CAST(VARBINARY AS <string type>) to the decoded bytes is right for
ordinary text, but FE does not evaluate two kinds of text like BE:

- StringLikeLiteral.compareTo takes the bytes after a NUL for padding, so
  CAST(X'610062' AS STRING) = 'a' folded to TRUE; BE compares all three bytes
  and returns FALSE.
- VARCHAR(n) truncation and trim_in work on UTF-16 units, so
  CAST(X'F09F9880' AS VARCHAR(1)) and trim_in(CAST(X'F09F9880' AS STRING),
  '<U+1F601>') kept half of U+1F600, which encodes as '?'; BE keeps the whole
  character.

Plain string literals such as 'a\0b' or a literal emoji reach the same FE
code. Before the decoded fold these binary inputs folded to their hex text,
which happened to give BE's answer here. castValueToString() now throws for
text with a NUL or a supplementary character, as it does for invalid UTF-8,
and FoldConstantRuleOnFE.visitCast leaves such a cast for BE.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…ies and BE folding

### What problem does this PR solve?

Issue Number: None

Related PR: apache#68968, apache#68930

Problem Summary: FE leaves a cast of VARBINARY bytes to a string type for BE when no FE string literal can carry its result: bytes that are not valid UTF-8, or text with a NUL byte or a supplementary character. Master always folded such a cast to the hex text of the bytes, so three places had never met it in a plan. Two are in FE and are fixed here:

- FE: 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: 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.

The third is in BE: ScanOperator digests every conjunct for the condition cache, which is on by default, and VLiteral::get_digest hashed the literal's column, which ColumnVarbinary does not support, so WHERE s = CAST(X'FF' AS STRING) failed with "update_xxHash_with_value is not supported for ColumnVarbinary". apache#68930 has since made VLiteral::get_digest return 0 for a VARBINARY column, which keeps such a conjunct out of the condition cache, so no BE change is needed here.

The .out of test_binary_hex_function recorded the folded hex text for sql_25 (61 for X'61'), while its sql_24 already expected the bytes for the same values read from a column; it is regenerated.

### Release note

None

### Check List (For Author)

- Test: Regression test (test_varbinary_cast_fold runs every query with FE folding, with FE and BE folding, and without folding, with the condition cache on; test_binary_hex_function regenerated) and Unit Test (FE: FoldConstantRuleOnBETest.testSkipCastFromVarBinary, VarBinarySqlSupportTest.testJoinConditionComparingStringsCastFromBinary)
- Behavior changed: Yes. A scan conjunct that keeps such a cast does not use the condition cache (apache#68930), and a cast from VARBINARY is not folded on BE.
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@morningman
morningman dismissed stale reviews from yujun777 and 924060929 via 344590f October 15, 2026 09:42
@morningman
morningman force-pushed the b2m-t35g-varbinary-cast-fold branch from 6b6e19d to 344590f Compare October 15, 2026 09:42
@morningman

Copy link
Copy Markdown
Contributor Author

run buildall

@morningman

Copy link
Copy Markdown
Contributor Author

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes: the narrowed VARBINARY join check still admits a nested binary hash key that fails in the shuffle exchange. The earlier scalar literal-folding findings are fixed at this head and are not reposted.

[P1] Reject nested VARBINARY keys before shuffle hashing: comparesVarBinary checks only each comparison operand's immediate type, so ARRAY<VARBINARY> equality passes the post-rewrite check and is extracted as a hash conjunct. In a partitioned join, the exchange invokes Crc32HashPartitioner::_do_hash/Crc32CHashPartitioner::_do_hash; ColumnArray recursively calls update_crcs_with_value/update_crc32c_batch on its nested ColumnVarbinary, whose hash methods intentionally throw NOT_IMPLEMENTED_ERROR (the unsupported behavior is covered by ColumnVarbinaryTest.HashingIsNotSupported). The serialized in-node hash path runs only after this exchange, so a forced-shuffle join on two array-of-binary keys fails before the join executes. Check the operand type recursively, for example with TypeCoercionUtils.hasVarBinaryType(operand.getDataType()), or add compatible nested-binary exchange hashing, and add a forced-shuffle ARRAY<VARBINARY> regression. This is distinct from the existing scalar CAST(VARBINARY AS STRING) join thread.

Critical checkpoint conclusions:

  1. Goal and proof: the PR changes VARBINARY-to-string FE folding from hex text to byte-preserving UTF-8 literals and defers values FE cannot represent. The added unit/regression paths cover ordinary text, malformed bytes, NUL/supplementary guards, optional BE folding, filters, and mark-join planning; the new P1 shows nested binary hash exchange handling is incomplete.
  2. Scope and clarity: the literal/folding changes are focused; the join helper is the narrow change that introduced the nested-key gap.
  3. Concurrency and locks: no new threads, shared mutable state, locks, or lock-order changes; the failing path is synchronous exchange hashing.
  4. Lifecycle/static initialization: only local decoder/buffer state is added; no new ownership, callbacks, cycles, or static-order dependency.
  5. Configuration: no configuration is added; existing FE folding, BE folding, condition-cache, strict-cast, and shuffle settings were traced.
  6. Compatibility: no thrift/protobuf definitions, storage formats, symbols, or FE-BE variables change. Existing BE cast and exchange interfaces are reused.
  7. Parallel paths: scalar and nested casts, STRING/CHAR/VARCHAR, strict/TRY_CAST, FE and optional BE folding, hash/mark joins, and serialized versus shuffle hashing were inspected. The nested ARRAY exchange path is the accepted finding; MAP/STRUCT comparison and mark-IN paths remain rejected by existing legality checks.
  8. Error handling: malformed UTF-8 is deferred instead of fabricated as NULL; the accepted exchange path reaches existing throwing unsupported hash APIs because the new FE check does not recurse.
  9. Data correctness: direct decoded strings now preserve BE bytes under the guarded paths, but an accepted ARRAY<VARBINARY> shuffle key can fail at runtime. No transaction, visible-version, delete-bitmap, or persistence path is changed.
  10. Test coverage: tests and goldens were inspected for all changed files and mode matrices; no builds or tests were run under the static-only review contract. A nullable IN/NOT IN unmatched-NULL assertion is a future coverage improvement, not a demonstrated defect.
  11. Performance/memory/observability: UTF-8 decoding and recursive type checks are linear with local lifetimes; no separate memory, metric, or logging issue was substantiated.
  12. Existing review context: the prior P1 NUL-cast finding is fixed by the current FE deferral and BE-fold skip, and all existing P2 threads are either fixed or pre-existing/duplicated; no existing P0/P1 ID is carried.

User focus: no additional focus was supplied. The complete changed-file scope, required AGENTS.md guides, existing threads, tests/goldens, and related FE/BE mechanisms were reviewed. This was static-only; author-reported runs and checked-in goldens were not treated as executed test evidence.

Review converged after 2 rounds: all three round-2 reviewers returned NO_NEW_VALUABLE_FINDINGS, the sole accepted M1 finding was independently confirmed, and every other candidate was deduplicated or dismissed with evidence.

Review converged after 2 round(s).

// 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()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Reject nested VARBINARY keys before shuffle hashing: comparesVarBinary checks only each comparison operand's immediate type, so ARRAY<VARBINARY> equality passes the post-rewrite check and is extracted as a hash conjunct. In a partitioned join, the exchange invokes Crc32HashPartitioner::_do_hash/Crc32CHashPartitioner::_do_hash; ColumnArray recursively calls update_crcs_with_value/update_crc32c_batch on its nested ColumnVarbinary, whose hash methods intentionally throw NOT_IMPLEMENTED_ERROR. The serialized in-node hash path runs only after this exchange, so a forced-shuffle join on two array-of-binary keys fails before the join executes. Check the operand type recursively (for example with TypeCoercionUtils.hasVarBinaryType(operand.getDataType())) or add compatible nested-binary exchange hashing, and add a forced-shuffle ARRAY<VARBINARY> regression.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this here: it does not come from this PR, and master behaves the same. Master's check, conjunct.anyMatch(e -> e.getDataType().isVarBinaryType()), tests each expression node's own type, so an ARRAY<VARBINARY> slot never matched it either; this PR only narrows which operands are checked. On a local cluster with ARRAY<VARBINARY> keys built from numbers(), JOIN [broadcast] returns the expected 24 rows and JOIN [shuffle] fails with [NOT_IMPLEMENTED_ERROR]Method update_crc32c_single is not supported for ColumnVarbinary, with or without this PR. Rejecting nested VARBINARY keys in this check would turn the broadcast joins that work today into analysis errors. What is missing is the exchange hash in the BE: #69142 gives ColumnVarbinary the CRC32 / CRC32C hashes (the same bytes hash as a STRING does), which the Iceberg binary partition routing needs as well, and adds a broadcast-versus-shuffle ARRAY<VARBINARY> join regression.

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 29242 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit 344590ff62aebc7537222fa0622db11ae7c8f2f0, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17594	5607	5558	5558
q2	2080	302	264	264
q3	10244	1409	838	838
q4	4689	472	342	342
q5	7423	803	532	532
q6	183	184	153	153
q7	753	797	603	603
q8	9252	1340	1097	1097
q9	5477	4286	4262	4262
q10	6811	1294	1040	1040
q11	433	255	239	239
q12	631	407	284	284
q13	18218	3899	2997	2997
q14	288	280	254	254
q15	q16	745	734	683	683
q17	1218	745	558	558
q18	7064	6244	6186	6186
q19	1134	975	605	605
q20	391	343	232	232
q21	5326	2250	2400	2250
q22	375	314	265	265
Total cold run time: 100329 ms
Total hot run time: 29242 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	6183	6130	6112	6112
q2	717	575	531	531
q3	5230	5390	4977	4977
q4	2001	2109	1556	1556
q5	5279	5486	5981	5486
q6	269	199	162	162
q7	2403	2135	1911	1911
q8	3283	3057	2772	2772
q9	8071	7993	8224	7993
q10	3729	3668	3341	3341
q11	648	431	418	418
q12	682	733	525	525
q13	3535	3874	3324	3324
q14	321	321	280	280
q15	q16	736	728	658	658
q17	8389	7538	7383	7383
q18	13177	12338	13073	12338
q19	911	795	793	793
q20	2184	2201	1947	1947
q21	5800	4709	4794	4709
q22	504	446	401	401
Total cold run time: 74052 ms
Total hot run time: 67617 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 152234 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit 344590ff62aebc7537222fa0622db11ae7c8f2f0, data reload: false

query5	4339	579	466	466
query6	440	221	201	201
query7	4822	450	233	233
query8	319	172	163	163
query9	8783	3981	3960	3960
query10	480	324	269	269
query11	6077	3789	3519	3519
query12	160	93	90	90
query13	1265	466	338	338
query14	6149	4052	3759	3759
query14_1	3537	3538	3526	3526
query15	207	203	181	181
query16	966	491	426	426
query17	999	664	552	552
query18	2423	431	315	315
query19	211	164	136	136
query20	87	95	83	83
query21	213	138	114	114
query22	12986	13117	12840	12840
query23	13692	12841	12411	12411
query23_1	12606	12609	12554	12554
query24	7086	837	469	469
query24_1	492	481	494	481
query25	535	442	372	372
query26	1277	268	140	140
query27	2735	452	275	275
query28	4608	1972	1991	1972
query29	1580	615	457	457
query30	302	227	193	193
query31	1012	934	815	815
query32	148	103	92	92
query33	524	306	252	252
query34	963	894	495	495
query35	748	754	641	641
query36	798	835	735	735
query37	138	102	88	88
query38	1795	1750	1695	1695
query39	807	817	748	748
query39_1	739	733	742	733
query40	211	124	94	94
query41	65	62	65	62
query42	85	80	86	80
query43	365	385	333	333
query44	1303	700	711	700
query45	175	173	166	166
query46	834	947	559	559
query47	3016	3012	2938	2938
query48	297	311	217	217
query49	574	394	298	298
query50	666	279	216	216
query51	10577	10395	10508	10395
query52	79	79	71	71
query53	189	205	149	149
query54	245	198	175	175
query55	74	69	65	65
query56	235	228	228	228
query57	1611	1595	1540	1540
query58	293	263	253	253
query59	2123	2180	1963	1963
query60	276	240	221	221
query61	147	146	144	144
query62	389	342	289	289
query63	198	168	162	162
query64	2778	928	767	767
query65	3293	3236	3216	3216
query66	1778	415	306	306
query67	20034	19962	20062	19962
query68	2949	1019	618	618
query69	422	293	245	245
query70	865	837	778	778
query71	285	234	202	202
query72	2942	2618	2271	2271
query73	515	524	295	295
query74	4590	4458	4287	4287
query75	2335	2268	1945	1945
query76	2302	1008	590	590
query77	333	367	272	272
query78	9749	9641	9122	9122
query79	974	846	521	521
query80	1181	421	348	348
query81	568	309	272	272
query82	574	135	110	110
query83	300	193	185	185
query84	291	130	110	110
query85	875	419	378	378
query86	393	239	229	229
query87	1997	1958	1834	1834
query88	3620	2726	2728	2726
query89	346	298	254	254
query90	1897	181	177	177
query91	172	156	132	132
query92	101	88	89	88
query93	1013	987	583	583
query94	610	328	285	285
query95	586	409	328	328
query96	664	521	235	235
query97	2441	2442	2318	2318
query98	160	151	157	151
query99	787	763	656	656
Total cold run time: 235470 ms
Total hot run time: 152234 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 24.98 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit 344590ff62aebc7537222fa0622db11ae7c8f2f0, data reload: false

query1	0.01	0.01	0.01
query2	0.10	0.05	0.04
query3	0.26	0.12	0.11
query4	1.62	0.12	0.12
query5	0.24	0.25	0.25
query6	1.17	0.69	0.69
query7	0.04	0.01	0.00
query8	0.06	0.04	0.04
query9	0.41	0.34	0.36
query10	0.56	0.57	0.57
query11	0.20	0.14	0.15
query12	0.18	0.15	0.14
query13	0.51	0.51	0.49
query14	1.01	1.02	1.02
query15	0.65	0.60	0.64
query16	0.36	0.37	0.36
query17	1.12	1.15	1.11
query18	0.22	0.22	0.20
query19	2.11	2.00	2.04
query20	0.02	0.01	0.01
query21	15.48	0.30	0.15
query22	4.48	0.06	0.06
query23	16.16	0.31	0.13
query24	2.60	0.75	0.72
query25	0.12	0.08	0.04
query26	1.88	0.27	0.16
query27	0.05	0.04	0.03
query28	2.40	0.64	0.41
query29	12.56	4.32	3.37
query30	0.29	0.14	0.13
query31	2.78	0.58	0.34
query32	3.23	0.62	0.51
query33	3.32	3.21	3.16
query34	15.42	4.03	3.40
query35	3.38	3.36	3.34
query36	0.55	0.50	0.43
query37	0.10	0.06	0.06
query38	0.05	0.03	0.04
query39	0.04	0.04	0.04
query40	0.17	0.15	0.14
query41	0.10	0.03	0.03
query42	0.04	0.03	0.03
query43	0.04	0.03	0.03
Total cold run time: 96.09 s
Total hot run time: 24.98 s

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 78.95% (15/19) 🎉
Increment coverage report
Complete coverage report

@yujun777 yujun777 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@924060929 924060929 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the FE literal conversion, deferred evaluation for unrepresentable strings, optional BE constant-folding guard, hash/mark join validation, and regression coverage. LGTM for the scoped fix; the previously discussed general nested VARCHAR folding limitation remains outside this change.

Static source review only; I did not independently run builds or tests.

@morningman
morningman merged commit a8b95fa into apache:master Oct 16, 2026
47 of 48 checks passed
morningman added a commit that referenced this pull request Oct 17, 2026
…o U+FFFD (#69143)

### What problem does this PR solve?

Issue Number: None

Related PR: #69025

Problem Summary:

**In short.** With `enable_fold_constant_by_be=true`, FE turned the
bytes of a string that BE computed for a constant into a literal by
decoding them as UTF-8 with replacement, so bytes that are not valid
UTF-8 became U+FFFD (`EF BF BD`). `WHERE s = unhex('FF')` then matched
the rows holding U+FFFD instead of the rows holding the byte `FF`, and
`SELECT unhex('FF')` returned `EF BF BD`. This PR leaves such a constant
unfolded, so BE evaluates it as it does with the variable off. Strings
that are valid UTF-8 fold as before. This was found while testing
#69025, which keeps its VARBINARY casts away from BE folding partly for
this reason.

**Background.** With `enable_fold_constant_by_be=true`,
`FoldConstantRuleOnBE` sends the largest constant subtrees of an
expression to a BE and replaces them with literals built from the values
BE returns (`getResultExpression`). BE writes the bytes of a string
value as they are into `PValues.string_value`, a proto2 `string` field
that does not require UTF-8, and FE read them with `getStringValue(i)`,
which decodes with replacement. A FE string literal is a Java string, so
it holds only text that is valid UTF-8. FE has no rule of its own for
`unhex`, so with the variable off the call stays in the plan and BE
evaluates it on the real bytes. The variable is off by default;
regression pipelines turn it on at random.

**The problem, and what it cost.** A table `t(k, s)` holds `'a'` (1),
`FF` (2), U+FFFD, `EFBFBD` (3), `61FF` (4), NULL (5) and `'é'`, `C3A9`
(6):

| Query | BE folding off, or `debug_skip_fold_constant=true` | master,
`enable_fold_constant_by_be=true` | with this PR |
|---|---|---|---|
| `SELECT k FROM t WHERE s = unhex('FF')` | 2 | **3** | 2 |
| `... WHERE s = concat('a', unhex('FF'))` | 4 | **no row** | 4 |
| `... WHERE array_contains(array(unhex('FF')), s)`, and the same with a
MAP key or a STRUCT field | 2 | **3** | 2 |
| `SELECT hex(concat(s, unhex('FF'))) FROM t WHERE k = 1` | `61FF` |
**`61EFBFBD`** | `61FF` |
| `SELECT unhex('FF')`, bytes sent to the client | `FF` | **`EF BF BD`**
| `FF` |
| `EXPLAIN ... WHERE s = unhex('FF')` | `s = unhex('FF')` | `s = '�'` |
`s = unhex('FF')` |
| `EXPLAIN ... WHERE s = unhex('C3A9')` | `s = unhex('C3A9')` | `s =
'é'` | `s = 'é'` |

The query succeeds either way, so the wrong rows and bytes go unnoticed:
a session variable changes which rows a filter on binary data matches.

**How this PR fixes it.** For a string type, `getResultExpression` reads
the bytes (`getStringValueBytes(i)`) and builds the literal only when
`ByteString.isValidUtf8()` holds; otherwise it returns no literal, which
`evalOnBE` already treats as "keep the expression" (as for a NaN float).
That check rejects exactly the bytes a Java string cannot carry: stray
bytes, truncated sequences, overlong forms, surrogates and code points
beyond U+10FFFF. An array, map or struct holding such a string is not
folded either, since those branches already stop when an element is not
folded. A valid string decodes to the same text as before, NUL bytes and
supplementary characters included.

The JSON branch is unchanged: BE never folds a JSON expression
(`shouldSkipFold`), and a JSON value inside a folded array, map or
struct can only come from a CAST that parses text, and BE's parser
rejects text that is not valid UTF-8 (`CAST(array(unhex('22FF22')) AS
ARRAY<JSON>)` is `[null]` with and without BE folding).

**Results.**

- **FE UT.**
`LiteralTest.testGetResultExpressionLeavesInvalidUtf8Unfolded`: `FF`,
`61FF`, `C3`, `C080` and `EDA080` are not folded as STRING, VARCHAR or
CHAR, nor as an ARRAY element, a MAP key or value, or a STRUCT field;
the same results with `C3A9` fold.
`testGetResultExpressionStringKeepsItsBytes`: valid UTF-8 with a NUL
byte, U+1F600 or U+FFFD itself folds to the same bytes. Without the fix
the first test fails. All tests in `LiteralTest` pass.
- **Regression.** The new `fold_constant_by_be_invalid_utf8`
(query_p0/expression/fold_constant) runs the filters and projections
above with FE folding, with BE folding and with folding skipped; the
results of each block must match in the three modes, and two `EXPLAIN`
checks show that `unhex('C3A9')` is still folded on BE and `unhex('FF')`
is not. Its `.out` was generated on a local cluster with this change;
with the FE before this change the suite fails at `filter_fold_by_be`
(row 3 instead of 2).
- **End to end.** The table above, on the same local cluster before and
after this change.

### Release note

With `enable_fold_constant_by_be=true`, a constant string whose bytes
are not valid UTF-8, such as `unhex('FF')`, keeps its bytes instead of
turning into U+FFFD, so filters and results match those with the
variable off.

### Check List (For Author)

- Test <!-- At least one of them must be included. -->
    - [x] Regression test
- `fold_constant_by_be_invalid_utf8`
(query_p0/expression/fold_constant): filters and projections on
`unhex('FF')` and the strings, arrays, maps and structs built from it,
in three folding modes.
    - [x] Unit Test
- `LiteralTest.testGetResultExpressionLeavesInvalidUtf8Unfolded` and
`LiteralTest.testGetResultExpressionStringKeepsItsBytes`.
    - [x] Manual test (add detailed scripts or steps below)
- The queries above on a local cluster, before and after this change.
    - [ ] No need to test or manual test. Explain why:
- [ ] This is a refactor/code format and no logic has been changed.
        - [ ] Previous test can cover this change.
        - [ ] No code files have been changed.
        - [ ] Other reason <!-- Add your reason?  -->

- Behavior changed:
    - [ ] No.
    - [x] Yes. <!-- Explain the behavior change -->
- With `enable_fold_constant_by_be=true`, a constant whose string result
is not valid UTF-8 is no longer folded; BE evaluates it, and FE logs the
existing `Be constant folding convert ... failed` warning.

- Does this need documentation?
    - [x] No.
- [ ] Yes. <!-- Add document PR link here. eg:
apache/doris-website#1214 -->

### Check List (For Reviewer who merge this PR)

- [ ] Confirm the release note
- [ ] Confirm test cases

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
morrySnow pushed a commit to morrySnow/incubator-doris that referenced this pull request Nov 4, 2026
### What problem does this PR solve?

Related PR: apache#69025

Problem Summary: VARBINARY 转字符串 FE 折叠成十六进制文本,与 BE 保留原始字节不一致

Backport adaptation: Keep branch legacy-VARIANT join restrictions, timestamp/date compatibility helpers and VarBinary equality handling. Include the required BE condition-cache fix apache#68930 with its unit/regression tests: unfolder VARBINARY/complex object literals must disable unsupported digest hashing rather than throw. Both FE and BE validation are required for this backport. The branch Literal base has no master castValueToString hook: override VarBinaryLiteral.deprecatingUncheckedCastTo for string-like targets and delegate decoded values to the existing StringLiteral conversion, preserving other literal conversions. Add CHAR, VARCHAR truncation and identity-cast assertions. Route both direct and nested-container fallback conversions through the same decoding hook; retain the invalid-UTF8 array rejection test and add a valid array conversion assertion. Use const accessors for Const/Nullable/Array/Map/Struct digest recursion because the branch mutable traversal moves offsets and updates variant state; keep existing scalar/variant hashing behavior.

CI follow-up: Regenerate send_long_data with the standard regression runner. Exactly five binary HEX cells change from the legacy double encoding 3030464638303431 to the correct raw-byte encoding 00FF8041: three streamed executions and two force-forward-mode executions. MD5, empty streams, inline strings, scalar parameters and connection reuse results are unchanged. Full ASAN build and all four required regression suites passed on the isolated cluster; production and unit-test sources are unchanged.

### Release note

VARBINARY 转字符串 FE 折叠成十六进制文本,与 BE 保留原始字节不一致

### Check List (For Author)

- Test: Unit Test — org.apache.doris.nereids.rules.expression.FoldConstantRuleOnBETest,org.apache.doris.nereids.rules.expression.FoldConstantTest,org.apache.doris.nereids.types.VarBinarySqlSupportTest; FE Checkstyle passed. BE ASAN build, formatting, targeted unit tests and clang-tidy passed; see local validation record. Static analysis used a task-local VFS overlay to remove an orphan NOLINTEND comment from the unchanged baseline types.h; production files and enabled checks were unchanged. Header hygiene gate unavailable: branch-4.2 does not contain build-support/check-build-hygiene.sh or its gates. Cluster regression tests passed: prepared_stmt_p0/send_long_data, query_p0/condition_cache/test_condition_cache_complex_literal, query_p0/sql_functions/binary_functions/test_binary_hex_function, query_p0/sql_functions/binary_functions/test_varbinary_cast_fold.
- Behavior changed: Yes, fixes the behavior described above.
- Does this need documentation: No

(cherry picked from commit a8b95fa)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants