Skip to content

[fix](fe) Fix nested column pruning OFFSET dedup crash on array<struct> - #62631

Merged
hello-stephen merged 1 commit into
apache:masterfrom
englefly:fix-length-str-offset
Apr 23, 2026
Merged

hello-stephen merged 1 commit into
apache:masterfrom
englefly:fix-length-str-offset

Conversation

@englefly

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: close #xxx

Related PR: #62205

Problem Summary:

PR #62205 introduced the length(str) OFFSET optimization for nested column pruning. When length() is applied to a string-like nested field, the pruner marks the access path with an OFFSET suffix so BE can read only the offset array instead of full element data.

However, the OFFSET path dedup logic operates at slot granularity: if ANY non-OFFSET path exists for the same slot, ALL OFFSET paths are stripped. This is incorrect for array<struct<...>> columns where different struct fields have independent access patterns.

Bug 1 (crash): Given a query like:

SELECT array_match_all(x -> length(struct_element(x, 'str_field')) > 0, arr),
       struct_element(element_at(arr, 1), 'int_field')
FROM t

The int_field non-OFFSET path causes the str_field OFFSET path to be stripped. BE never reads str_field data → crash or wrong results.

Fix: Replace slot-level OFFSET dedup with per-field bidirectional prefix matching. An OFFSET path P+["OFFSET"] is only stripped when a non-OFFSET path Q shares a prefix relationship with P (covering the same container or ancestor), not when Q accesses a sibling struct field.

Bug 2 (wrong pruning): When isStringOffsetOnly=true AND accessPartialChild=true (e.g., cardinality(arr) + arr[*].f1), pruneDataType() returns the full type instead of pruning unused fields.

Fix: Add !accessPartialChild guard to the isStringOffsetOnly check.

Release note

Fix a BE crash caused by nested column pruning incorrectly stripping OFFSET access paths for array columns when different struct fields have independent access patterns (e.g., length() on one field + direct access on another).

Check List (For Author)

  • Test: Regression test
  • Behavior changed: No
  • Does this need documentation: No

What problem does this PR solve?

Issue Number: close #xxx

Related PR: #xxx

Problem Summary:

Release note

None

Check List (For Author)

  • Test

    • Regression test
    • Unit Test
    • 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:

    • No.
    • Yes.
  • Does this need documentation?

    • No.
    • Yes.

Check List (For Reviewer who merge this PR)

  • Confirm the release note
  • Confirm test cases
  • Confirm document
  • Add branch pick label

@englefly

Copy link
Copy Markdown
Contributor Author

/review

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

@englefly

Copy link
Copy Markdown
Contributor Author

run buildall

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

One blocking issue remains in the new OFFSET dedup logic.

Critical checkpoint conclusions:

  • Goal of the current task: partially achieved. The array sibling-field case from the PR description is covered, but map value accesses that are spelled as * vs VALUES are still treated as unrelated, so OFFSET stripping is not correct end-to-end.
  • Scope/focus: the patch is small and localized.
  • Concurrency: no concurrency-sensitive code is involved here.
  • Lifecycle/static init: no special lifecycle concerns.
  • Config/compatibility/persistence/FE-BE protocol: no new config, compatibility, or persistence concerns.
  • Parallel code paths: not fully handled. Map value access has semantically equivalent encodings (* and VALUES), and the new raw-prefix comparison only handles one spelling.
  • Special conditional checks: the new minLen prefix test is too weak because it compares raw path tokens instead of normalized semantic coverage.
  • Tests: the added regression covers the array crash path, but I did not find coverage for the still-broken map overlap case, e.g. length(map_col['a']) together with map_values(map_col)[1].
  • Test result changes: the new .out file looks consistent with the added SQL case.
  • Observability: no additional logging or metrics are needed for this optimizer fix.
  • Transaction/persistence/data writes: not applicable.
  • Performance: no material concern in this change.
  • Other issues: none beyond the blocking bug below.

}
int minLen = Math.min(nonOffset.size(), prefix.size());
if (minLen == 0
|| nonOffset.subList(0, minLen).equals(prefix.subList(0, minLen))) {

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.

This raw-prefix comparison misses overlapping map-value accesses that are spelled with different path tokens. length(map_col['a']) is collected as [map_col, *, OFFSET], but map_values(map_col)[1] becomes [map_col, VALUES]. Those two paths still target the same value container, yet * and VALUES are not prefix-equal here, so the OFFSET path survives.

That leaves both val_col.OFFSET and val_col on the BE side. MapFileColumnIterator::set_access_paths() forwards both to the value iterator, and StringFileColumnIterator::set_access_paths() switches to OFFSET_ONLY as soon as it sees the first one, so the full-value consumer reads zero-filled strings instead of the real payload.

A query like select length(map_col['a']), map_values(map_col)[1] from ... should still suppress the OFFSET path. The dedup needs to normalize the map aliases (* vs VALUES/KEYS) or compare semantic container coverage instead of raw string prefixes.

@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage 31.82% (7/22) 🎉
Increment coverage report
Complete coverage report

@englefly

Copy link
Copy Markdown
Contributor Author

/review

@englefly

Copy link
Copy Markdown
Contributor Author

run buildall

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

Findings

  • Blocking: fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/NestedColumnPruning.java
    The new * vs VALUES semantic equivalence is not safe in both directions. [map_col, *] is not just value access; it is also the FE signal that BE must read map keys so element_at(map_col, key) can locate the entry. If a query mixes length(map_col['a']) with map_values(map_col)[1], the new dedup drops [map_col, *, OFFSET] and keeps only [map_col, VALUES]. In BE, MapFileColumnIterator::set_access_paths() then marks the key iterator SKIP_READING, so the element_at(map_col, 'a') inside length() can no longer match the key and returns NULL/defaults. The new EXPLAIN-only regression test for this case therefore encodes the broken behavior instead of guarding against it.

Critical Checkpoints

  • Goal: The array dedup fix and the !accessPartialChild guard look correct, but the map */VALUES change does not accomplish the goal safely because it regresses mixed element_at + map_values queries.
  • Scope/minimality: The patch is small and focused.
  • Concurrency: Not involved.
  • Lifecycle/static init: Not involved.
  • Config: None.
  • Compatibility / FE-BE contract: Affected. The new equivalence violates the existing access-path contract in BE, where * implies key + value access and VALUES implies value-only access.
  • Parallel code paths: Not fully handled. Any path that uses element_at(map, key) still needs keys even when another expression reads only VALUES.
  • Special conditions: The new suppression condition is missing the required asymmetry between * and VALUES.
  • Test coverage: Good coverage for the array fix; insufficient for the map case because the new test only checks EXPLAIN and does not verify query results.
  • Test result files: The added .out file appears auto-generated and consistent with the executed case.
  • Observability: No new observability needs.
  • Transaction/persistence/data writes: Not applicable.
  • FE-BE variable/path passing: No new fields, but the encoded access path is wrong for the mixed map case.
  • Performance: No material concern beyond the correctness issue.
  • Other issues: None beyond the blocking regression above.

Requesting changes because the current map-path suppression can produce wrong results.

// Both cover the same value container, so treat them as equivalent for OFFSET suppression.
// Do NOT treat * as equivalent to KEYS. For example:
// select length(map_col['a']), map_keys(map_col) from t
// map_keys(map_col) only covers the key side, while length(map_col['a']) still needs

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.

This comparison is not safe in both directions. A [map_col, *] path means element_at(map, key) style access, and the BE uses it to keep the map key iterator readable as well as the value iterator (see MapFileColumnIterator::set_access_paths()). [map_col, VALUES] only preserves the value side. With this change, select length(map_col['a']), map_values(map_col)[1] ... drops [map_col, *, OFFSET] and keeps only [map_col, VALUES]; BE then sets the key iterator to SKIP_READING, so the element_at(map_col, 'a') inside length() can no longer match the key and returns NULL/defaults. The * vs VALUES relation here needs to be directional, or this case needs to be rewritten to KEYS + VALUES rather than removing the * path entirely.

@englefly

Copy link
Copy Markdown
Contributor Author

/review

@englefly

Copy link
Copy Markdown
Contributor Author

run buildall

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

Requesting changes because one correctness gap remains.

Critical checkpoints:

  • Goal/task: The PR aims to make OFFSET-path dedup safe for mixed nested accesses. It fixes the top-level ARRAY/MAP cases covered by the new tests, but it does not yet cover the same access pattern when the container is nested under a STRUCT slot.
  • Scope/focus: The change is localized to NestedColumnPruning, but the new guard if (slot.getDataType().isArrayType() || slot.getDataType().isMapType()) scopes the rewrite too narrowly.
  • Concurrency: Not applicable; this is FE-only rewrite logic with no new concurrent state.
  • Lifecycle/config/compatibility: No new lifecycle, config, or upgrade concerns found.
  • Parallel code paths: The BE access-path contract is the same for array/map children inside structs, so those paths need the same suppression/rewrite behavior.
  • Special conditions: The current special handling only triggers when the root slot itself is ARRAY/MAP; nested containers under STRUCT still forward conflicting OFFSET and full-data paths together.
  • Test coverage: Added FE unit/regression tests cover top-level arr_struct and map_col cases, but they do not cover STRUCT<arr/map ...> cases where the bug still reproduces.
  • Observability/persistence/transactions/FE-BE variables: No additional blocking issues found.
  • Validation: I attempted bash run-fe-ut.sh --run org.apache.doris.nereids.rules.rewrite.PruneNestedColumnTest, but this runner is missing thirdparty/installed/bin/protoc, so I could not rerun FE UT locally.

See the inline comment for the concrete failing shape.

@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage 51.46% (53/103) 🎉
Increment coverage report
Complete coverage report

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 71.84% (74/103) 🎉
Increment coverage report
Complete coverage report

@englefly

Copy link
Copy Markdown
Contributor Author

run external

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 71.84% (74/103) 🎉
Increment coverage report
Complete coverage report

@englefly

Copy link
Copy Markdown
Contributor Author

/review

@englefly

Copy link
Copy Markdown
Contributor Author

run buildall

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

Blocking finding:

  • regression-test/conf/regression-conf.groovy was committed with worktree-local port overrides. This file is explicitly local-only and AGENTS.md says env/run config changes must not be included in the PR.

Critical checkpoint conclusions:

  • Goal of the task: The FE pruning change appears to address the reported array<struct> sibling-field case and the mixed element_at(map) + map_values(map) case. The added FE unit tests and regression tests exercise those paths.
  • Small / focused change: Not fully. The code fix itself is focused, but the PR also includes an unrelated local regression config edit.
  • Concurrency: Not involved. I did not find shared-state or lock-order concerns in this change.
  • Lifecycle / static initialization: Not involved.
  • Configuration items: No new product config is added, but a checked-in regression harness config file was modified with local ports and should not be merged.
  • Compatibility: No FE/BE protocol, symbol, or storage-format compatibility issue found in the pruning logic.
  • Parallel code paths: I traced the collector -> NestedColumnPruning -> slot replacement -> BE map/array/struct iterator flow. The new path-rewrite logic is consistent with BE's distinction between *, KEYS, VALUES, and OFFSET.
  • Special conditionals: The new type-aware compareOffsetPrefixCoverage() logic is justified for the affected map/array cases.
  • Test coverage: Coverage for the newly fixed select-list cases is good in both FE UT and regression tests. I did not find another concrete blocker in the code path.
  • Test result files: The new .out files are consistent with the new order_qt cases.
  • Observability: Not needed for this optimizer-side fix.
  • Transaction / persistence / data write / FE-BE variable passing: Not applicable here.
  • Performance: The additional per-slot path comparison work is small and acceptable for the expected number of collected access paths.
  • Other issues: None beyond the accidental config check-in.

// add allowLoadLocalInfile so that the jdbc can execute mysql load data from client.
jdbcUrl = "jdbc:mysql://127.0.0.1:9030/?useLocalSessionState=true&allowLoadLocalInfile=true&zeroDateTimeBehavior=round"
targetJdbcUrl = "jdbc:mysql://127.0.0.1:9030/?useLocalSessionState=true&allowLoadLocalInfile=true&zeroDateTimeBehavior=round"
jdbcUrl = "jdbc:mysql://127.0.0.1:39131/?useLocalSessionState=true&allowLoadLocalInfile=true&zeroDateTimeBehavior=round"

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.

This looks like worktree-local test setup rather than PR content. regression-test/conf/regression-conf.groovy is the shared default harness config, and AGENTS.md explicitly says env/run config changes should not be committed. If this lands, CI and other developers will start targeting 39131/39121/38131 instead of the repo defaults. Please drop this file from the PR.

@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage 60.95% (64/105) 🎉
Increment coverage report
Complete coverage report

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 47.62% (80/168) 🎉
Increment coverage report
Complete coverage report

@englefly

Copy link
Copy Markdown
Contributor Author

run p0

@englefly

Copy link
Copy Markdown
Contributor Author

run cloud_p0

@englefly

Copy link
Copy Markdown
Contributor Author

run nonConcurrent

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 33.20% (80/241) 🎉
Increment coverage report
Complete coverage report

1 similar comment
@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 33.20% (80/241) 🎉
Increment coverage report
Complete coverage report

@englefly

Copy link
Copy Markdown
Contributor Author

run cloud_p0

@englefly

Copy link
Copy Markdown
Contributor Author

/review

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 32.65% (80/245) 🎉
Increment coverage report
Complete coverage report

@englefly

Copy link
Copy Markdown
Contributor Author

run buildall

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 76.19% (80/105) 🎉
Increment coverage report
Complete coverage report

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 65.04% (80/123) 🎉
Increment coverage report
Complete coverage report

2 similar comments
@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 65.04% (80/123) 🎉
Increment coverage report
Complete coverage report

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 65.04% (80/123) 🎉
Increment coverage report
Complete coverage report

@englefly
englefly force-pushed the fix-length-str-offset branch from e10d5f1 to cf12c43 Compare April 23, 2026 08:13
@englefly

Copy link
Copy Markdown
Contributor Author

run buildall

@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage 60.95% (64/105) 🎉
Increment coverage report
Complete coverage report

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 76.19% (80/105) 🎉
Increment coverage report
Complete coverage report

@github-actions github-actions Bot added the approved Indicates a PR has been approved by one committer. label Apr 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

PR approved by at least one committer and no changes requested.

@hello-stephen
hello-stephen merged commit e143cb2 into apache:master Apr 23, 2026
31 of 32 checks passed
@englefly
englefly deleted the fix-length-str-offset branch April 24, 2026 01:37
englefly added a commit to englefly/incubator-doris that referenced this pull request May 9, 2026
…t> (apache#62631)

### What problem does this PR solve?

Issue Number: close #xxx

Related PR: apache#62205

Problem Summary:

PR apache#62205 introduced the `length(str)` OFFSET optimization for nested
column pruning. When `length()` is applied to a string-like nested
field, the pruner marks the access path with an OFFSET suffix so BE can
read only the offset array instead of full element data.

However, the OFFSET path dedup logic operates at **slot granularity**:
if ANY non-OFFSET path exists for the same slot, ALL OFFSET paths are
stripped. This is incorrect for `array<struct<...>>` columns where
different struct fields have independent access patterns.

**Bug 1 (crash):** Given a query like:
```sql
SELECT array_match_all(x -> length(struct_element(x, 'str_field')) > 0, arr),
       struct_element(element_at(arr, 1), 'int_field')
FROM t
```
The `int_field` non-OFFSET path causes the `str_field` OFFSET path to be
stripped. BE never reads `str_field` data → crash or wrong results.

**Fix:** Replace slot-level OFFSET dedup with per-field bidirectional
prefix matching. An OFFSET path `P+["OFFSET"]` is only stripped when a
non-OFFSET path Q shares a prefix relationship with P (covering the same
container or ancestor), not when Q accesses a sibling struct field.

**Bug 2 (wrong pruning):** When `isStringOffsetOnly=true` AND
`accessPartialChild=true` (e.g., `cardinality(arr) + arr[*].f1`),
`pruneDataType()` returns the full type instead of pruning unused
fields.

**Fix:** Add `!accessPartialChild` guard to the `isStringOffsetOnly`
check.

### Release note

Fix a BE crash caused by nested column pruning incorrectly stripping
OFFSET access paths for array<struct> columns when different struct
fields have independent access patterns (e.g., length() on one field +
direct access on another).

Co-authored-by: Copilot <[email protected]>
zhaorongsheng pushed a commit to zhaorongsheng/doris that referenced this pull request Jun 4, 2026
…t> (apache#62631)

### What problem does this PR solve?

Issue Number: close #xxx

Related PR: apache#62205

Problem Summary:

PR apache#62205 introduced the `length(str)` OFFSET optimization for nested
column pruning. When `length()` is applied to a string-like nested
field, the pruner marks the access path with an OFFSET suffix so BE can
read only the offset array instead of full element data.

However, the OFFSET path dedup logic operates at **slot granularity**:
if ANY non-OFFSET path exists for the same slot, ALL OFFSET paths are
stripped. This is incorrect for `array<struct<...>>` columns where
different struct fields have independent access patterns.

**Bug 1 (crash):** Given a query like:
```sql
SELECT array_match_all(x -> length(struct_element(x, 'str_field')) > 0, arr),
       struct_element(element_at(arr, 1), 'int_field')
FROM t
```
The `int_field` non-OFFSET path causes the `str_field` OFFSET path to be
stripped. BE never reads `str_field` data → crash or wrong results.

**Fix:** Replace slot-level OFFSET dedup with per-field bidirectional
prefix matching. An OFFSET path `P+["OFFSET"]` is only stripped when a
non-OFFSET path Q shares a prefix relationship with P (covering the same
container or ancestor), not when Q accesses a sibling struct field.

**Bug 2 (wrong pruning):** When `isStringOffsetOnly=true` AND
`accessPartialChild=true` (e.g., `cardinality(arr) + arr[*].f1`),
`pruneDataType()` returns the full type instead of pruning unused
fields.

**Fix:** Add `!accessPartialChild` guard to the `isStringOffsetOnly`
check.

### Release note

Fix a BE crash caused by nested column pruning incorrectly stripping
OFFSET access paths for array<struct> columns when different struct
fields have independent access patterns (e.g., length() on one field +
direct access on another).
englefly added a commit to englefly/incubator-doris that referenced this pull request Sep 20, 2026
…array<struct> (apache#62631)

### What problem does this PR solve?

Issue Number: close #xxx

Related PR: apache#62205

Problem Summary:

PR apache#62205 introduced the `length(str)` OFFSET optimization for nested
column pruning. When `length()` is applied to a string-like nested
field, the pruner marks the access path with an OFFSET suffix so BE can
read only the offset array instead of full element data.

However, the OFFSET path dedup logic operates at **slot granularity**:
if ANY non-OFFSET path exists for the same slot, ALL OFFSET paths are
stripped. This is incorrect for `array<struct<...>>` columns where
different struct fields have independent access patterns.

**Bug 1 (crash):** Given a query like:
```sql
SELECT array_match_all(x -> length(struct_element(x, 'str_field')) > 0, arr),
       struct_element(element_at(arr, 1), 'int_field')
FROM t
```
The `int_field` non-OFFSET path causes the `str_field` OFFSET path to be
stripped. BE never reads `str_field` data → crash or wrong results.

**Fix:** Replace slot-level OFFSET dedup with per-field bidirectional
prefix matching. An OFFSET path `P+["OFFSET"]` is only stripped when a
non-OFFSET path Q shares a prefix relationship with P (covering the same
container or ancestor), not when Q accesses a sibling struct field.

**Bug 2 (wrong pruning):** When `isStringOffsetOnly=true` AND
`accessPartialChild=true` (e.g., `cardinality(arr) + arr[*].f1`),
`pruneDataType()` returns the full type instead of pruning unused
fields.

**Fix:** Add `!accessPartialChild` guard to the `isStringOffsetOnly`
check.

### Release note

Fix a BE crash caused by nested column pruning incorrectly stripping
OFFSET access paths for array<struct> columns when different struct
fields have independent access patterns (e.g., length() on one field +
direct access on another).
yiguolei pushed a commit that referenced this pull request Sep 21, 2026
… path series (#68214)

Cherry-pick of #62205, #62315, #62631, #62304, #63229, #63736, #64486 to
branch-4.2 (the meta path series, applied in merge order).

Adaptations for branch-4.2:
* `AccessPathInfo` already lives in `org.apache.doris.analysis` on this
branch; the new `ACCESS_STRING_OFFSET` constant was ported there
(aliasing the existing `ACCESS_OFFSET`).
* master-only types renamed to this branch's thrift types
(`ColumnAccessPath* -> TColumnAccessPath`, `ColumnAccessPathType ->
TAccessPathType`); `NestedColumnPruning` keeps branch-4.2's
`comparePathSegments`/variant branch alongside upstream's new logic.
* `LazyMaterializeTopN` merges upstream's restructure while preserving
branch-4.2's Lance lazy-materialization gate;
`LogicalJoin.getRightConditionSlot()` (a master-only helper) had to be
added for the ported `PushDownTopNThroughJoin`.
* `StringEmptyToLengthRuleTest` uses `ElementAt` (branch-4.2 folded
`StructElement` into `ElementAt`); the regression suite expectation for
the same reason.

REQUIRED EXTRA COMMIT: the FE now emits offset-only / null-only nested
access paths, which branch-4.2's BE could not read —
`null_column_pruning.groovy` aborted the BE with
`std::vector<...ColumnIterator>::operator[]: Assertion '__n <
this->size()' failed`. This PR therefore also cherry-picks the BE-side
prerequisite #61888 "[Exec](be) Support offset prue column and null
column in BE" (adapted to branch-4.2's `IColumn::mutate`/Defer COW shape
and `ColumnNullable::get_null_map_column_ptr()` signature). Please
review that extra commit carefully.

Testing on branch-4.2 (FE + BE rebuilt from this branch):
* FE unit tests: PruneNestedColumnTest (57),
PullUpProjectExprUnderTopNTest (28), OperativeColumnDeriveTest (4),
StringEmptyToLengthRuleTest (11), MaterializeProbeVisitorTest (8) — 108
tests, 0 failures.
* Regression: nereids_rules_p0/column_pruning (7 suites incl. the new
string_length/null/nested_container_offset/topn_expr_pullup/topn_lazy_nested
suites) 0 failures; shape_check
clickbench/tpcds_sf100/tpcds_sf1000/tpcds_sf10t_orc/tpcds_sf1000_constraints
suites touched by the series regenerated with -forceGenOut and green;
nereids_rules_p0 push_filter_through / null_un_safe_equals /
lazy_materialize_topn green.
* Known deviations: #62446 (struct field slot type for OFFSET-only
access) is not part of the requested series and was not included; the
BE-side `MapAccessAllWithOffsetDoesNotPropagateOffsetToKey` unit test
from #61888 was kept byte-identical to upstream although it may not hold
(test binary not built with MAKE_TEST=ON).

---------

Co-authored-by: Copilot <[email protected]>
Co-authored-by: Claude Opus 4.7 <[email protected]>
Co-authored-by: HappenLee <[email protected]>
yiguolei pushed a commit that referenced this pull request Oct 8, 2026
…-named nested fields (#65805) (#68314)

### What problem does this PR solve?

Cherry-pick of #65805 (`[fix](nereids) Disambiguate NULL/OFFSET metadata
from same-named nested fields`) to `branch-4.2`.

The port also carries the two prerequisite commits that #65805 is
written on top of, because branch-4.2 stopped at the state just before
them:

| commit | PR | why it is needed |
| --- | --- | --- |
| `branch-4.2 [fix](NestedColumnPruning) Rewrite the NestedColumnPruning
meta-path handling ...` | #64535 | branch-4.2's FE pruning code was
exactly master-before-#64535; #65805's FE hunks are written against the
rewritten meta-path handling. |
| `branch-4.2 [feat](olap) Support lazy reading mode for pruned complex
columns` | #59263 | on master #59263 replaced #64535's `MetaPathStriper`
approach again (predicate metadata paths are kept,
`normalizePredicateMetaPathForAllAccessPath` /
`addPredicatePathsToFinalAllAccessPaths` / `expandMapStarPaths`), and
that is the code #65805 patches. |
| `branch-4.2 [fix](nereids) Disambiguate NULL/OFFSET metadata from
same-named nested fields` | #65805 | the requested pick. |

Master order for reference: `#61888 → #62205 → #62315 → #62631 → #62304
→ #63229 → #63736 → #64486 → #64535 → #59263 → … → #65805`; branch-4.2
already had everything up to #64486 (ported by #68214).

### What #65805 fixes

* **Versioned, type-selected access paths.** `TColumnAccessPath` gets an
optional `version` (`TCOLUMN_ACCESS_PATH_VERSION_LEGACY = 0`,
`TCOLUMN_ACCESS_PATH_VERSION_TYPED = 1`) that is carried through thrift,
protobuf and both FE/BE descriptor conversions. In the typed format the
path type is authoritative: `DATA` selects `data_access_path`, `META`
selects `meta_access_path`, and `NULL`/`OFFSET` are only ever emitted as
typed `META` paths. New BEs still decode the legacy all-`DATA` encoding,
so the supported rolling upgrade is: upgrade all BEs first, then let FEs
send typed paths.
* **FE.** Marks `NULL`/`OFFSET` collector contexts as `META`; checks the
physical column's nullability instead of slot nullability, so an
outer-join-nullable `NOT NULL` column no longer gets a `[col, NULL]`
path; falls back to plain data reads for variant sub-columns; keeps the
exact meta path when a sibling data path is also read; and keys pruning
on the path type instead of string suffixes.
* **BE.** Validates path versions and type-selected payloads; partitions
current data / current metadata / descendant routing with explicit
per-container ownership; derives `NULL_MAP_ONLY` / `OFFSET_ONLY` only
when no current or predicate data path requires payload; keeps sibling
`META` paths explicit; routes Map children by the logical
`KEYS`/`VALUES` selectors instead of physical child column names; and
rejects unrecognized Map selectors instead of silently pruning
everything.

### branch-4.2 adaptations

* branch-4.2 keeps the FE access-path model as thrift types instead of
master's `ColumnAccessPath` / `DescriptorToThriftConverter`, so the
typed version is stamped in `NestedColumnPruning.buildColumnAccessPaths`
(the branch's single producer) and preserved by
`SlotTypeReplacer.replaceIcebergAccessPathToId`.
* The master-only `DescriptorToThriftConverter` /
`DescriptorToThriftConverterTest` files do not exist on this branch and
were not added; the equivalent coverage lives in
`PruneNestedColumnTest`, whose `path()` / `metaPath()` helpers (and
`VariantPruningLogicTest`'s) now carry the typed version.
* Ported FE code was mapped onto this branch's names (`TAccessPathType`,
`TColumnAccessPath`, `isFunctionNullCheckPath`) and keeps the
segment-wise access-path comparator introduced by the branch's
nested-column-pruning port.
* `gensrc/thrift/PaloInternalService.thrift`: master uses field id `226`
for `enable_prune_nested_column`, but branch-4.2 already uses
`226`-`228`, so the option was moved to the next free id (`229`).
* `segment_iterator.cpp` keeps branch-4.2's `_non_predicate_column_ids`
naming and drops the master-only `_update_lsn_col_if_needed` /
`_update_tso_col_if_needed` calls from the ported hunk; branch-4.2 keeps
its own `common/compile_check_begin.h` include in front of the new
`ScopedColumnIteratorReadPhase` helper. `_read_lazy_pruned_columns` uses
the branch's `_schema_block_id_map` because master's
`Schema::column_index()` does not exist here, and
`FileColumnIterator::get_reader()` was added as in master.
* Regression suites `lambda_null_pruning` and
`map_contains_arg_pruning`, and the unit tests for lambda access paths /
Map lazy-read-by-rowids, only exist on master (they come from PRs
outside this chain) and were not added.

### Test

* FE: `./build.sh --fe` OK. `./run-fe-ut.sh --run
'org.apache.doris.nereids.rules.rewrite.PruneNestedColumnTest'` → 63
passed; `VariantPruningLogicTest` 14, `IcebergScanNodeTest` 95,
`MaterializeProbeVisitorTest` 8, `StringEmptyToLengthRuleTest`,
`PullUpProjectExprUnderTopNTest` — all green.
* BE: `./build.sh --be` OK (the two ported BE test sources were
additionally checked with the unit-test flags, `-DBE_TEST
-fno-access-control`).
* Regression: `./run-regression-test.sh --run -d
nereids_rules_p0/column_pruning` → 8 suites, 0 failed, including the new
`left_join_not_null_column` suite that guards the outer-join null-map
crash.

### Behavior changed

Yes. Typed `META` access paths with an explicit version replace the
legacy all-`DATA` encoding between new FEs and BEs (upgrade BEs before
FEs); struct fields literally named `NULL`/`OFFSET` are now read
correctly instead of being pruned as metadata; `IS NULL` on an
outer-join-nullable `NOT NULL` column no longer crashes the BE; invalid
Map descendant selectors now return an internal error instead of being
silently ignored. The port also brings #59263, which defers reading
non-predicate pruned complex columns until after predicate evaluation,
reducing nested-column I/O.

### Release note

None

### Rebased onto branch-4.2 (2026-09-23)

The branch was rebased onto the current `branch-4.2` (`c0cbcc3e49c`) and
is now a single commit `e044385682f`. The two prerequisite ports listed
above are folded into it: branch-4.2 has since gained its own
nested-column-pruning series (`#68214`) and the `#68331` predicate-path
fix, and `#59263` had already replaced the `#64535` `MetaPathStriper`
design inside this pick, so replaying the intermediate states is no
longer meaningful. See the comment below for the conflict-resolution
details and the verification results.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by one committer. dev/4.2.0-merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants