morrySnow commented on code in PR #65805:
URL: https://github.com/apache/doris/pull/65805#discussion_r3765966189


##########
be/src/storage/segment/column_reader.cpp:
##########
@@ -951,44 +1186,122 @@ void 
ColumnIterator::_recovery_from_place_holder_column(MutableColumnPtr& dst) {
     }
 }
 
-Result<TColumnAccessPaths> ColumnIterator::_get_sub_access_paths(
-        TColumnAccessPaths sub_access_paths, bool is_predicate) {
-    // Access paths passed to a complex iterator always start with the current
-    // column name. Strip that component and return the remaining 
child-relative
-    // paths to the caller. For example, when this iterator is for column `s`,
-    // path `s.a.b` is converted to `a.b` and then dispatched to child `a`.
-    //
-    // If stripping the current column consumes the whole path, the current
-    // iterator itself is requested rather than one of its children. Mark the
-    // current iterator according to the path source: predicate paths must be 
read
-    // in the predicate phase, while all/output paths become lazy output 
targets.
-    // Empty or mismatched paths indicate an FE/BE access-path contract 
violation.
-    for (auto it = sub_access_paths.begin(); it != sub_access_paths.end();) {
-        TColumnAccessPath& name_path = *it;
-        if (name_path.data_access_path.path.empty()) {
+Result<ColumnIterator::AccessPathSplit> ColumnIterator::_split_access_paths(
+        TColumnAccessPaths access_paths) const {
+    AccessPathSplit split;
+    for (auto& path : access_paths) {
+        const bool uses_legacy_encoding = 
uses_legacy_access_path_encoding(path);
+        if (!uses_legacy_encoding &&
+            path.version != 
g_Descriptors_constants.TCOLUMN_ACCESS_PATH_VERSION_TYPED) {
+            return ResultError(
+                    Status::InternalError("Unsupported access path version: 
{}", path.version));
+        }
+
+        std::vector<std::string>* components = nullptr;
+        if (uses_legacy_encoding) {
+            if (path.type != TAccessPathType::DATA) {
+                return ResultError(Status::InternalError("Invalid legacy 
access path type: {}",
+                                                         
static_cast<int>(path.type)));
+            }
+            if (!path.__isset.data_access_path) {
+                return ResultError(Status::InternalError(
+                        "Invalid legacy access path: data_access_path payload 
is not set"));
+            }
+            components = &path.data_access_path.path;
+        } else {
+            switch (path.type) {
+            case TAccessPathType::DATA:
+                if (!path.__isset.data_access_path) {
+                    return ResultError(Status::InternalError(
+                            "Invalid DATA access path: data_access_path 
payload is not set"));
+                }
+                components = &path.data_access_path.path;
+                break;
+            case TAccessPathType::META:
+                if (!path.__isset.meta_access_path) {
+                    return ResultError(Status::InternalError(
+                            "Invalid META access path: meta_access_path 
payload is not set"));
+                }
+                components = &path.meta_access_path.path;
+                break;
+            default:
+                return ResultError(Status::InternalError("Invalid access path 
type: {}",
+                                                         
static_cast<int>(path.type)));
+            }
+        }
+
+        if (components->empty()) {
             return ResultError(Status::InternalError(
                     "Invalid access path for column '{}': path is empty", 
_column_name));
         }
 
-        if (!StringCaseEqual()(name_path.data_access_path.path[0], 
_column_name)) {
+        if (!StringCaseEqual()((*components)[0], _column_name)) {
             return ResultError(Status::InternalError(
                     R"(Invalid access path for column: expected name "{}", got 
"{}")", _column_name,
-                    name_path.data_access_path.path[0]));
+                    (*components)[0]));
         }
 
-        
name_path.data_access_path.path.erase(name_path.data_access_path.path.begin());
-        if (!name_path.data_access_path.path.empty()) {
-            ++it;
-        } else {
-            if (is_predicate) {
-                set_read_requirement(ReadRequirement::PREDICATE);
-            } else {
-                set_lazy_output_requirement();
+        components->erase(components->begin());
+        if (components->empty()) {
+            split.reads_current_data = true;
+            continue;
+        }
+
+        const bool is_current_level_meta =

Review Comment:
   Legacy-encoding regression for structs that have a field literally named 
`OFFSET` (the PR itself adds `meta_name_tbl struct<`NULL`: string, `OFFSET`: 
string>` as a supported schema).
   
   A legacy DATA path `[s, OFFSET]` from an old FE now matches 
`is_current_level_meta` here and is consumed as OFFSET_ONLY metadata, so it 
never reaches `descendant_paths`. The struct then skips every child 
(`need_to_read` is false for all of them in 
`StructFileColumnIterator::set_access_paths`), and the field data is silently 
not read.
   
   The pre-PR struct `set_access_paths()` behaved differently: 
`remove_current_level_meta_access_paths(sub_all_access_paths)` only ran when a 
current-level data path existed, so `[OFFSET]` stayed in `sub_all_access_paths` 
and was routed to the child field named `OFFSET`; the OFFSET_ONLY mode was 
harmless for structs because only NULL_MAP_ONLY skipped children. So `select 
element_at(s, 'OFFSET') from t` returned correct data before, and now silently 
returns wrong values with old FE + new BE — the exact compatibility case stated 
in the PR description ("New BEs continue to decode the legacy all-DATA format 
from old FEs") — during the rolling-upgrade window.
   
   For legacy-encoded paths, consider treating a single NULL/OFFSET component 
as current-level metadata only when the current struct has no child field with 
that exact name, preserving the old routing; the typed-path behavior 
(`TypedDataFieldsNamedMetaComponentsAreNotTreatedAsMetaPaths`) already handles 
the unambiguous case.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/AccessPathExpressionCollector.java:
##########
@@ -334,6 +362,23 @@ public Void visitElementAt(ElementAt elementAt, 
CollectorContext context) {
             Expression fieldName = arguments.get(1);
             DataType fieldType = fieldName.getDataType();
             if (fieldName.isLiteral() && (fieldType.isIntegerLikeType() || 
fieldType.isStringLikeType())) {
+                // Only emit META [s, field, NULL] when the selected field 
itself is nullable.
+                if (context.type == ColumnAccessPathType.META
+                        && 
isUnderIsNull(context.accessPathBuilder.getPathList())) {
+                    StructField field = resolveStructField(
+                            (StructType) first.getDataType(), fieldName);
+                    if (field == null || !field.isNullable()) {

Review Comment:
   When the selected struct field is not nullable, this branch replaces the 
META context with a fresh DATA context, which drops the trailing NULL suffix 
and emits `[s, f]` DATA instead of `[s, f, NULL]` META.
   
   This is correct for evaluation but misses a more precise read: for 
`element_at(s, 'f') IS NULL` with s physically nullable and f NOT NULL, the 
predicate is equivalent to `s IS NULL`, so `[s, NULL]` META alone would suffice 
— it also preserves the full struct type in pruneDataType, which is the stated 
purpose of this fallback — while the current code reads the entire field 
column. Consider emitting `[s, NULL]` META when the field is not nullable and 
`hasPhysicalNullMap(slotReference)` is true, instead of falling back to full 
field data.



##########
fe/fe-core/src/main/java/org/apache/doris/analysis/DescriptorToThriftConverter.java:
##########
@@ -88,6 +89,7 @@ public static TSlotDescriptor toThrift(SlotDescriptor 
slotDesc) {
     public static TColumnAccessPath toThrift(ColumnAccessPath accessPath) {
         TColumnAccessPath result = new TColumnAccessPath(
                 accessPath.getType() == ColumnAccessPathType.DATA ? 
TAccessPathType.DATA : TAccessPathType.META);
+        
result.setVersion(DescriptorsConstants.TCOLUMN_ACCESS_PATH_VERSION_TYPED);

Review Comment:
   All access paths now unconditionally carry version=1, including queries that 
produce only DATA paths. A pre-upgrade BE ignores the version field but fails 
hard on any META path (`_get_sub_access_paths()` dereferences 
`data_access_path.path`, which is unset), so a new FE running against 
not-yet-upgraded BEs turns every IS NULL / length() / cardinality() 
nested-column query into an error instead of degrading gracefully. Combined 
with the legacy path ambiguity on the BE side, only the documented BE-first 
upgrade order is safe; the failure mode of the reverse order (hard query 
errors, not just missed optimizations) may be worth stating in the PR 
description.



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