This is an automated email from the ASF dual-hosted git repository.

CalvinKirs pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 5b2ba1898ce [fix](iceberg) Fix nested Variant access paths and stale 
Variant profile checks (#68137)
5b2ba1898ce is described below

commit 5b2ba1898ce7e715b819601310fddd9367052255
Author: daidai <[email protected]>
AuthorDate: Fri Sep 18 10:36:22 2026 +0800

    [fix](iceberg) Fix nested Variant access paths and stale Variant profile 
checks (#68137)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: None
    
    Problem Summary:
    
    `test_iceberg_variant_read` times out waiting for
    `VariantDirectLeafPathMisses` in its profile checks.
    
    BE now extracts keys from unshredded Parquet Variant values by seeking
    the binary value directly. That path records
    `VariantUnshreddedDirectSeekRows` instead of missing the typed leaf and
    reconstructing the row, so `VariantDirectLeafPathMisses` stays at 0 and
    leaf-only queries no longer reconstruct Variant rows.
    
    This PR:
    - asserts `VariantUnshreddedDirectSeekRows` for the unshredded files;
    - drops the reconstruction checks on queries that only read a leaf;
    - reports each required counter's sum when a profile wait times out, so
    a remaining zero counter is visible in CI;
    - stops requiring `FilteredRowsByPage` in the page-pruning check. CAST
    is not safe to pre-execute, so the metadata-pruning fence keeps the
    Variant predicate out of page pruning and the counter is always 0;
    - emits the `VARIANT` type for Variant leaves in the Iceberg schema
    dictionary. `IcebergSchemaUtils` wrote them as a `STRING` placeholder,
    so filtering a Variant nested in a STRUCT (`info.payload['x']`) failed
    in BE with `AccessPathParser does not support access path info.6 for
    slot payload`.
---
 .../connector/iceberg/IcebergSchemaUtils.java      |  7 +++
 .../iceberg/test_iceberg_variant_read.groovy       | 63 +++++++++++-----------
 2 files changed, 40 insertions(+), 30 deletions(-)

diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergSchemaUtils.java
 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergSchemaUtils.java
index e156b63181b..208b52f2166 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergSchemaUtils.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergSchemaUtils.java
@@ -397,6 +397,13 @@ public final class IcebergSchemaUtils {
             tField.setType(buildPrimitiveColumnType(type, enableVarbinary, 
enableTimestampTz));
             return tField;
         }
+        if (type.isVariantType()) {
+            // Variant object keys are data, not schema fields. BE resolves a 
Variant access path nested in a
+            // STRUCT/ARRAY/MAP only when this leaf keeps its VARIANT type 
instead of a scalar placeholder.
+            columnType.setType(TPrimitiveType.VARIANT);
+            tField.setType(columnType);
+            return tField;
+        }
 
         TNestedField nestedField = new TNestedField();
         switch (type.typeId()) {
diff --git 
a/regression-test/suites/external_table_p0/iceberg/test_iceberg_variant_read.groovy
 
b/regression-test/suites/external_table_p0/iceberg/test_iceberg_variant_read.groovy
index f8daed1664d..34fb7a612aa 100644
--- 
a/regression-test/suites/external_table_p0/iceberg/test_iceberg_variant_read.groovy
+++ 
b/regression-test/suites/external_table_p0/iceberg/test_iceberg_variant_read.groovy
@@ -486,12 +486,20 @@ public class AppendVariantEqualityDelete {
         if (positiveCounters.every { String counter -> counterSum(lastProfile, 
counter) > 0 }) {
             return lastProfile
         }
-        return profileAction.waitProfile({
-            lastProfile = profileAction.getProfileBySql(token, 
positiveCounters)
-            return positiveCounters.every {
-                String counter -> counterSum(lastProfile, counter) > 0
-            } ? lastProfile : ""
-        }, [], "Completed profile with positive counters ${positiveCounters} 
for ${token}")
+        try {
+            return profileAction.waitProfile({
+                lastProfile = profileAction.getProfileBySql(token, 
positiveCounters)
+                return positiveCounters.every {
+                    String counter -> counterSum(lastProfile, counter) > 0
+                } ? lastProfile : ""
+            }, [], "Completed profile with positive counters 
${positiveCounters} for ${token}")
+        } catch (IllegalStateException e) {
+            // The wait only reports an empty profile, so name the counters 
that stayed at zero.
+            Map<String, Long> sums = positiveCounters.collectEntries { String 
counter ->
+                [(counter): counterSum(lastProfile, counter)]
+            }
+            throw new IllegalStateException("${e.getMessage()}counter sums: 
${sums}", e)
+        }
     }
 
     String evolutionInitial = latestSnapshotId("variant_evolution")
@@ -651,8 +659,8 @@ public class AppendVariantEqualityDelete {
         WHERE v['shared'] >= 20
         ORDER BY id
     """
-    // The stable snapshot contributes a genuinely shredded file, while the 
appended file uses
-    // the unshredded fallback. More than four rows qualify, forcing local 
TopN overshoot to be
+    // The stable snapshot contributes a genuinely shredded file, while the 
appended file is read by
+    // seeking its unshredded value. More than four rows qualify, forcing 
local TopN overshoot to be
     // truncated after the merge exchange while the mapper-eligible projected 
path crosses the wire.
     explain {
         sql """
@@ -685,11 +693,11 @@ public class AppendVariantEqualityDelete {
     """
     assertEquals(4, projectedGatherRows.size())
     String projectedGatherProfile = getProfileByToken(projectedGatherToken,
-            ["VariantLeafProjections", 
"VariantDirectLeafPathMisses"]).toString()
+            ["VariantLeafProjections", 
"VariantUnshreddedDirectSeekRows"]).toString()
     assertTrue(counterSum(projectedGatherProfile, "VariantLeafProjections") > 
0,
             "The projected TopN did not read a physical shredded Variant leaf")
-    assertTrue(counterSum(projectedGatherProfile, 
"VariantDirectLeafPathMisses") > 0,
-            "The projected TopN did not combine the unshredded fallback file")
+    assertTrue(counterSum(projectedGatherProfile, 
"VariantUnshreddedDirectSeekRows") > 0,
+            "The projected TopN did not combine the unshredded Variant file")
     order_qt_variant_projected_remote_gather """
         SELECT id,
                CAST(projected['n'] AS INT)
@@ -728,14 +736,12 @@ public class AppendVariantEqualityDelete {
         WHERE CAST(v['n'] AS INT) >= 8000
     """
     String multiRowGroupColdProfile = getProfileByToken(multiRowGroupColdToken,
-            ["RowGroupsTotalNum", "VariantDirectLeafPathMisses", 
"VariantReconstructedRows",
+            ["RowGroupsTotalNum", "VariantUnshreddedDirectSeekRows",
              "FilteredRowsByLazyRead"]).toString()
     assertTrue(counterSum(multiRowGroupColdProfile, "RowGroupsTotalNum") > 1,
                "The generated Variant file did not contain multiple Parquet 
row groups")
-    assertTrue(counterSum(multiRowGroupColdProfile, 
"VariantDirectLeafPathMisses") > 0,
-               "The unshredded scan did not record its direct-leaf fallback")
-    assertTrue(counterSum(multiRowGroupColdProfile, 
"VariantReconstructedRows") > 0,
-               "The unshredded scan did not reconstruct Variant rows")
+    assertTrue(counterSum(multiRowGroupColdProfile, 
"VariantUnshreddedDirectSeekRows") > 0,
+               "The unshredded scan did not seek its predicate leaf")
     assertTrue(counterSum(multiRowGroupColdProfile, "FilteredRowsByLazyRead") 
> 0,
                "The unshredded Variant predicate did not defer non-predicate 
columns")
     String multiRowGroupWarmToken =
@@ -746,9 +752,9 @@ public class AppendVariantEqualityDelete {
         WHERE CAST(v['n'] AS INT) >= 8000
     """
     String multiRowGroupWarmProfile = getProfileByToken(multiRowGroupWarmToken,
-            ["VariantDirectLeafPathMisses"]).toString()
-    assertTrue(counterSum(multiRowGroupWarmProfile, 
"VariantDirectLeafPathMisses") > 0,
-               "The warm unshredded scan did not preserve its direct-leaf 
fallback")
+            ["VariantUnshreddedDirectSeekRows"]).toString()
+    assertTrue(counterSum(multiRowGroupWarmProfile, 
"VariantUnshreddedDirectSeekRows") > 0,
+               "The warm unshredded scan did not seek its predicate leaf")
     qt_variant_multi_row_group_result """
         SELECT COUNT(*), MIN(id), MAX(id), SUM(CAST(v['n'] AS BIGINT))
         FROM variant_multi_row_group
@@ -822,7 +828,8 @@ public class AppendVariantEqualityDelete {
             "The shredded predicate did not defer complete Variant output")
 
     // The query projects the complete Variant while its predicate reads the 
shredded typed leaf.
-    // The appended unshredded file must fall back independently in the same 
scan.
+    // The appended unshredded file must be read independently in the same 
scan. CAST is not safe
+    // to pre-execute, so the metadata-pruning fence keeps this predicate out 
of page pruning.
     String pagePruningToken = "iceberg_variant_page_pruning_" + 
UUID.randomUUID().toString()
     sql """
         SELECT '${pagePruningToken}', id, CAST(v AS STRING)
@@ -831,16 +838,14 @@ public class AppendVariantEqualityDelete {
         ORDER BY id
     """
     String pagePruningProfile = getProfileByToken(pagePruningToken,
-            ["FilteredRowsByPage", "VariantLeafProjections", 
"VariantDirectLeafPathMisses",
+            ["VariantLeafProjections", "VariantUnshreddedDirectSeekRows",
              "VariantDirectLeafRows", "VariantReconstructedRows"]).toString()
-    assertTrue(counterSum(pagePruningProfile, "FilteredRowsByPage") > 0,
-               "Shredded Variant typed_value did not filter any Parquet page")
     // The predicate_access_paths contract keeps the typed leaf eager while 
the complete Variant
     // root is read through the independent deferred-output projection.
     assertTrue(counterSum(pagePruningProfile, "VariantLeafProjections") > 0,
                "A root Variant output query did not retain its typed predicate 
leaf projection")
-    assertTrue(counterSum(pagePruningProfile, "VariantDirectLeafPathMisses") > 
0,
-               "The mixed scan did not fall back for its unshredded Variant 
file")
+    assertTrue(counterSum(pagePruningProfile, 
"VariantUnshreddedDirectSeekRows") > 0,
+               "The mixed scan did not read its unshredded Variant file")
     assertTrue(counterSum(pagePruningProfile, "VariantDirectLeafRows") > 0,
                "The mixed scan did not evaluate rows from the shredded typed 
leaf")
     assertTrue(counterSum(pagePruningProfile, "VariantReconstructedRows") > 0,
@@ -1042,11 +1047,9 @@ public class AppendVariantEqualityDelete {
         WHERE v['n'] >= 40
     """
     String positionDeleteProfile = getProfileByToken(positionDeleteToken,
-            ["VariantDirectLeafPathMisses", 
"VariantReconstructedRows"]).toString()
-    assertTrue(counterSum(positionDeleteProfile, 
"VariantDirectLeafPathMisses") > 0,
-               "Position-delete filtering did not preserve the unshredded 
Variant fallback")
-    assertTrue(counterSum(positionDeleteProfile, "VariantReconstructedRows") > 
0,
-               "Position-delete filtering did not reconstruct its Variant 
rows")
+            ["VariantUnshreddedDirectSeekRows"]).toString()
+    assertTrue(counterSum(positionDeleteProfile, 
"VariantUnshreddedDirectSeekRows") > 0,
+               "Position-delete filtering did not seek the unshredded Variant 
leaf")
 
     // Files written before the Variant field existed have no physical Variant 
payload. Schema
     // evolution must synthesize NULL instead of rejecting their non-Parquet 
file format.


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

Reply via email to