Skip to content

[fix](nereids) Do not turn invalid UTF-8 from BE constant folding into U+FFFD - #69143

Merged
morningman merged 1 commit into
apache:masterfrom
morningman:b2m-t35i-fold-be-invalid-utf8
Oct 17, 2026
Merged

morningman merged 1 commit into
apache:masterfrom
morningman:b2m-t35i-fold-be-invalid-utf8

Conversation

@morningman

Copy link
Copy Markdown
Contributor

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

    • 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.
    • Unit Test
      • LiteralTest.testGetResultExpressionLeavesInvalidUtf8Unfolded and LiteralTest.testGetResultExpressionStringKeepsItsBytes.
    • 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
  • Behavior changed:

    • No.
    • Yes.
      • 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?

    • No.
    • Yes.

Check List (For Reviewer who merge this PR)

  • Confirm the release note
  • Confirm test cases

…o U+FFFD

### What problem does this PR solve?

Issue Number: None

Related PR: apache#69025

Problem Summary: With enable_fold_constant_by_be=true, FoldConstantRuleOnBE
built string literals from the values BE returned with
PValues.getStringValue(i), which decodes the bytes as UTF-8 with
replacement. BE writes the bytes of a string as they are, so bytes that are
not valid UTF-8 became U+FFFD: WHERE s = unhex('FF') matched rows holding
U+FFFD (EF BF BD) instead of the byte FF, s = concat('a', unhex('FF'))
matched nothing, an array, map or struct holding such a string did the
same, and SELECT unhex('FF') returned EF BF BD. With the variable off, or
with debug_skip_fold_constant=true, the results are right.

getResultExpression now reads the bytes with getStringValueBytes(i) and
builds the literal only when they are valid UTF-8; otherwise it returns no
literal, so the expression stays for BE to evaluate, as for a NaN float. An
array, map or struct holding such a string is not folded either. Valid
strings fold as before.

### 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: Regression test / Unit Test / Manual test
    - Regression test: fold_constant_by_be_invalid_utf8 runs filters and
      projections on unhex('FF') with FE folding, BE folding and folding
      skipped, and checks that the results match
    - Unit Test: LiteralTest, getResultExpression on invalid and valid UTF-8,
      also inside arrays, maps and structs
    - Manual test: the queries above on a local cluster before and after
- Behavior changed: Yes. With enable_fold_constant_by_be=true such constants
  are no longer folded; BE evaluates them, and the results match those with
  the variable off.
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@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.

Reviewed all four changed files at 2c4082e8fd9df6aeaa160540add9d80e489c9357. No new actionable findings or existing blocking inline comments were found.

The raw-byte UTF-8 check preserves invalid string results by retaining the original BE expression. Array, map and struct conversion propagates that decision, while valid UTF-8 continues to fold. All three reviewers returned NO_NEW_VALUABLE_FINDINGS after their final rechecks.

Critical checkpoint conclusions:

  1. Goal and proof: source tracing supports byte preservation; added unit and regression cases exercise the original corruption. Tests were inspected, not executed.
  2. Scope and reuse: one focused production branch reuses ByteString validation and the existing unsupported-result fallback.
  3. Concurrency and locks: new values are local/immutable; no shared-state or locking changes.
  4. Lifecycle and initialization: no new ownership, resource, thread, callback or static-initialization dependency.
  5. Configuration: no new settings; existing folding switches retain their gates.
  6. Compatibility: no symbol, storage-format or FE/BE protocol requirement changes.
  7. Thrift/protobuf IDs: no definition or field-ID changes.
  8. Parallel paths: STRING/CHAR/VARCHAR and nested children share the guard. JSON eligibility/parser and legacy protocol paths were checked; no distinct bypass was established.
  9. Conditions and errors: the comment explains the concrete invalid-byte case. Rejection retains the original subtree without partial literal replacement; equality checks stop unchanged rewrites.
  10. Test coverage: malformed encodings, valid NUL/supplementary/U+FFFD bytes, character types and complex values are covered. Regression predicates/projections compare three modes and check valid/invalid EXPLAIN behavior.
  11. Expected results: all seven golden blocks match the inserted bytes, filter matches, concatenation order and null propagation under static inspection. Ordered tags, session switches, cache aliases and Java 17 API compatibility were checked.
  12. Observability: existing fallback logging includes the expression and query ID; no additional metrics requirement identified.
  13. Transactions and persistence: no EditLog, failover, visible-version or delete-bitmap behavior changes.
  14. Writes and atomicity: production changes only optimizer result conversion; no transactional write-path changes.
  15. FE/BE variables: no new transmitted variable or request option.
  16. Performance: validation/decoding remain linear, without an added raw byte-array copy or a demonstrated rewrite-loop regression.
  17. Other optimizer invariants: rule registration/stage, slots, ExprIds, joins, distribution and plan wrappers remain unchanged; no further issue found.

No additional user focus was provided. Both initial risk focuses are resolved with source evidence, and no unresolved candidate or uncovered changed-file scope remains.

Static review only: no builds, tests, cluster queries or source changes were performed. Runtime behavior and the author's reported test execution/golden-generation provenance were not independently verified.

Review converged after 1 round(s).

@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17620	5628	5568	5568
q2	2109	349	245	245
q3	10234	1379	848	848
q4	4683	465	352	352
q5	7443	789	539	539
q6	187	191	164	164
q7	765	816	620	620
q8	9248	1304	1093	1093
q9	5610	4291	4267	4267
q10	6832	1291	1024	1024
q11	447	262	241	241
q12	641	397	287	287
q13	18112	3724	3081	3081
q14	275	280	252	252
q15	q16	727	728	676	676
q17	1164	733	581	581
q18	7123	6219	6155	6155
q19	1112	948	601	601
q20	391	339	227	227
q21	5155	2498	2657	2498
q22	433	370	289	289
Total cold run time: 100311 ms
Total hot run time: 29608 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	6541	6486	6498	6486
q2	738	617	562	562
q3	5587	5737	5191	5191
q4	2405	2158	1716	1716
q5	5782	5627	5513	5513
q6	241	199	160	160
q7	2365	2080	1871	1871
q8	3161	2771	2883	2771
q9	8193	7595	7546	7546
q10	3662	3596	3238	3238
q11	600	428	418	418
q12	675	693	520	520
q13	3282	3588	2978	2978
q14	289	296	263	263
q15	q16	664	697	616	616
q17	7870	7023	6905	6905
q18	13186	12262	13044	12262
q19	916	787	781	781
q20	2200	2193	1937	1937
q21	6045	4901	5141	4901
q22	532	483	431	431
Total cold run time: 74934 ms
Total hot run time: 67066 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 152937 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 2c4082e8fd9df6aeaa160540add9d80e489c9357, data reload: false

query5	4304	572	447	447
query6	449	213	201	201
query7	4802	446	227	227
query8	316	174	157	157
query9	8755	4013	4005	4005
query10	447	307	264	264
query11	6054	3774	3505	3505
query12	154	94	89	89
query13	1253	428	309	309
query14	6148	4052	3834	3834
query14_1	3627	3557	3578	3557
query15	207	197	179	179
query16	946	442	423	423
query17	849	714	526	526
query18	2399	413	298	298
query19	193	157	137	137
query20	83	83	83	83
query21	206	128	111	111
query22	12891	12975	12760	12760
query23	13494	12872	12544	12544
query23_1	12607	12604	12539	12539
query24	7111	819	457	457
query24_1	471	458	469	458
query25	498	393	341	341
query26	1290	252	134	134
query27	2775	433	277	277
query28	4610	1960	1961	1960
query29	1554	583	436	436
query30	297	212	184	184
query31	1018	919	824	824
query32	144	96	92	92
query33	506	295	237	237
query34	888	856	500	500
query35	753	760	635	635
query36	799	831	735	735
query37	130	129	86	86
query38	1794	1746	1669	1669
query39	793	802	749	749
query39_1	731	733	753	733
query40	217	109	99	99
query41	78	62	61	61
query42	85	81	83	81
query43	369	379	325	325
query44	1280	687	717	687
query45	178	176	160	160
query46	846	962	566	566
query47	2936	2946	2927	2927
query48	293	302	215	215
query49	564	394	301	301
query50	684	275	215	215
query51	10671	10707	10689	10689
query52	80	88	74	74
query53	182	212	160	160
query54	228	192	180	180
query55	72	67	62	62
query56	225	223	214	214
query57	1626	1594	1542	1542
query58	294	266	261	261
query59	2136	2123	1918	1918
query60	276	235	221	221
query61	143	149	147	147
query62	393	334	285	285
query63	195	164	184	164
query64	2729	981	890	890
query65	3307	3270	3234	3234
query66	1806	453	313	313
query67	20021	20142	19902	19902
query68	2872	1005	616	616
query69	390	294	262	262
query70	840	819	811	811
query71	296	235	212	212
query72	2774	2676	2552	2552
query73	513	531	351	351
query74	4593	4452	4318	4318
query75	2337	2272	1967	1967
query76	2287	1026	615	615
query77	337	377	289	289
query78	9880	9758	9178	9178
query79	987	839	515	515
query80	554	427	323	323
query81	533	317	264	264
query82	576	134	108	108
query83	251	195	178	178
query84	316	126	103	103
query85	793	431	368	368
query86	330	236	228	228
query87	2021	1942	1835	1835
query88	3630	2719	2710	2710
query89	335	298	259	259
query90	1877	179	167	167
query91	159	152	125	125
query92	96	85	84	84
query93	1011	1003	589	589
query94	453	309	280	280
query95	571	390	324	324
query96	675	523	232	232
query97	2452	2481	2324	2324
query98	206	150	148	148
query99	783	756	657	657
Total cold run time: 233746 ms
Total hot run time: 152937 ms

@hello-stephen

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

query1	0.00	0.00	0.00
query2	0.10	0.05	0.05
query3	0.26	0.11	0.12
query4	1.60	0.12	0.11
query5	0.28	0.25	0.21
query6	1.16	0.68	0.68
query7	0.04	0.01	0.00
query8	0.06	0.04	0.04
query9	0.40	0.35	0.36
query10	0.59	0.59	0.60
query11	0.20	0.14	0.14
query12	0.18	0.14	0.15
query13	0.50	0.50	0.50
query14	1.01	1.01	1.01
query15	0.62	0.62	0.61
query16	0.36	0.35	0.34
query17	1.13	1.16	1.13
query18	0.23	0.21	0.21
query19	2.22	2.02	2.01
query20	0.02	0.02	0.01
query21	15.47	0.26	0.14
query22	4.55	0.05	0.06
query23	16.21	0.30	0.12
query24	2.97	0.64	0.46
query25	0.11	0.06	0.06
query26	1.82	0.24	0.16
query27	0.05	0.04	0.03
query28	2.95	0.61	0.40
query29	12.57	4.23	3.33
query30	0.28	0.14	0.14
query31	2.77	0.57	0.34
query32	3.22	0.61	0.52
query33	3.21	3.21	3.20
query34	15.59	4.02	3.36
query35	3.33	3.33	3.32
query36	0.54	0.48	0.42
query37	0.09	0.06	0.06
query38	0.04	0.03	0.04
query39	0.04	0.03	0.03
query40	0.18	0.15	0.14
query41	0.09	0.03	0.03
query42	0.04	0.03	0.03
query43	0.04	0.03	0.03
Total cold run time: 97.12 s
Total hot run time: 24.64 s

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 100.00% (4/4) 🎉
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

@morningman
morningman merged commit 6c27d4d into apache:master Oct 17, 2026
39 checks passed
yiguolei pushed a commit that referenced this pull request Nov 3, 2026
… folding into U+FFFD #69143 (#70024)

picked from master #69143

Co-authored-by: Mingyu Chen (Rayner) <[email protected]>
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.

5 participants