englefly opened a new pull request, #69328:
URL: https://github.com/apache/doris/pull/69328

   ### What problem does this PR solve?
   
   Issue Number: DORIS-29733, DORIS-29734
   
   Related PR: None
   
   Problem Summary:
   
   A STRUCT -> STRUCT cast converts positionally and its result carries the 
target field names, but
   the FE treated a cast that only renames fields as a no-op and dropped the 
CAST node when
   serializing expressions to thrift: `CastExpr.noOp` comes from 
`Type.matchExactType`, whose struct
   branch compared the fields positionally and ignored their names. The 
folded-away cast also
   overwrote the child expression's type with the target type, so the FE 
bookkeeping and the physical
   column disagreed: the value kept the source field names, while the 
ELEMENT_AT binding and the
   nested column pruning of the cast target were planned against the target 
names.
   
   Reproduction with the default session variables on a single FE/BE:
   
   ```sql
   CREATE TABLE t (id INT, s STRUCT<a:STRING,b:STRING>) ENGINE = OLAP
   DUPLICATE KEY(id) DISTRIBUTED BY HASH(id) BUCKETS 1 PROPERTIES 
("replication_num" = "1");
   INSERT INTO t VALUES (1, named_struct('a', 'left', 'b', 'right'));
   
   SELECT element_at(CAST(s AS STRUCT<b:STRING,a:STRING>), 'b') FROM t;
   ```
   
   Before the fix:
   
   * with nested column pruning on (the default) the pruned slot kept only 
source field `a`, and the
     query failed with
     `errCode = 2, detailMessage = ... [RUNTIME_ERROR]Element not found for 
function element_at: name b not found in Struct(a:Nullable(String))`;
   * with nested column pruning off the query silently returned `right` (the 
source field `b`)
     instead of `left`;
   * reading both target fields returned (`right`, `left`) instead of (`left`, 
`right`), while the
     whole value CAST correctly returned `{b:'left', a:'right'}`.
   
   `EXPLAIN VERBOSE` is correct in every case because the Nereids plan keeps 
the cast; only the thrift
   sent to BE lost it, which is what makes this easy to misdiagnose as a 
pruning bug.
   
   Root cause in one line: `noOp == true` meant "no CAST_EXPR node", and BE's 
`element_at` resolves the
   field name against the type attached to the argument column in the Block 
(`try_get_position_by_name`
   on the struct type of the block position). With the cast gone, that type was 
the scan type, i.e.
   the source field names. BE has an explicit hook for this FE/BE type 
divergence
   (`VExpr::execute_type`, "for slot-ref expressions the type of the returned 
Column may not match
   data_type"), which is exactly the gap the elided cast fell into.
   
   Fix:
   
   A struct field name is part of the value identity, so `Type.matchExactType` 
now also requires the
   field names to match (comparing the already normalized lower case name, 
consistent with BE's case
   insensitive struct field lookup). A rename-only cast is serialized instead 
of being skipped, and BE
   relabels the value with the target type. Since the positional field types 
are unchanged, BE keeps
   the identity cast wrapper (`prepare_impl` compares with 
`DataTypeStruct::equals`, which ignores
   names), so no data is copied. Renames nested in ARRAY/MAP are covered by the 
same recursion, and
   `try_cast` inherits the fixed no-op decision.
   
   After the fix both toggle states return the positional result, b=`left` and 
a=`right`, matching the
   whole value CAST, and per-field pruning is preserved: the scan still reads 
only the source field
   behind the accessed target field (`all access paths: [s.a]` / `[s.b]`).
   
   ### Release note
   
   Fixed a wrong result and an "Element not found for function element_at" 
error when a
   STRUCT -> STRUCT cast renames fields and its result is accessed by field 
name.
   
   ### Check List (For Author)
   
   - Test
       - [x] Regression test
       - [x] Unit Test
       - [x] Manual test (add detailed scripts or steps below)
       - [ ] No need to test or manual test. Explain why:
   - Behavior changed:
       - [ ] No.
       - [x] Yes. A cast that only renames STRUCT fields is no longer folded 
away, so its result
         carries the target field names as the type system already promised. 
Casts whose field names
         already match keep the existing no-op behavior.
   - Does this need documentation?
       - [x] No.
   
   Test detail:
   
   * Unit test: `mvn test -pl fe-core -am 
-Dtest='CastExprHashPartitionTest,CastMonotonicityTest,CastTest,CheckCastTest,SimplifyCastRuleTest,TryCastTest,TypeTest,ExprToThriftBehaviorTest'`
 passes.
     `ExprToThriftBehaviorTest#testStructRenameCastIsNotNoOp` asserts `isNoOp() 
== false` and that the
     serialized tree is `CAST_EXPR(type=struct<b,a>)` + 
`SLOT_REF(type=struct<a,b>)`;
     `#testStructRenameNestedInArrayCastIsNotNoOp` covers the recursion;
     `#testSameStructTypeCastIsStillNoOp` pins that the no-op optimization is 
kept;
     `TypeTest#testStructWithNestedFieldsExactMatch` covers the match rule 
itself.
   * Regression test: `./run-regression-test.sh --run -d 
nereids_rules_p0/column_pruning` — 16 suites
     pass, including the new `struct_cast_rename_pruning`, which asserts the 
values and
     `all access paths: [s.a]` / `[s.b]` for pruning on and off.
   * Manual test: the snippet above plus WHERE / GROUP BY / ORDER BY / subquery 
/ CTE / `INSERT INTO` a
     table whose struct column is named differently / ARRAY and MAP nested 
renames, all with nested
     column pruning both on and off, on a local FE built from this commit.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to