This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-4.1 by this push:
new a4504d3138f branch-4.1: [fix](parquet) Keep Variant metadata pruning
behind the safe conjunct prefix (#68144)
a4504d3138f is described below
commit a4504d3138f68e89f94c8111fa9ce41711bfed9a
Author: daidai <[email protected]>
AuthorDate: Fri Sep 18 13:59:15 2026 +0800
branch-4.1: [fix](parquet) Keep Variant metadata pruning behind the safe
conjunct prefix (#68144)
### What problem does this PR solve?
Issue Number: None
Related PR: #68137
Problem Summary:
On branch-4.1, a Variant predicate that follows an unsafe conjunct can
prune Parquet row groups and pages before that conjunct is evaluated, so
its error is lost:
```sql
SELECT COUNT(*) FROM iceberg_tbl
WHERE assert_true(id != 1, 'barrier') AND v['n'] > 5000;
-- returns 0 instead of failing with 'barrier'
```
Ordinary metadata pruning only uses the conjuncts before the first
unsafe one (`metadata_pruning_safe_conjunct_count`). The shredded
Variant row-group statistics, page-index pruning and
`has_variant_shredded_filter` still read every conjunct. This PR
restricts them to the same prefix, matching master.
As on master, CAST is not safe to pre-execute, so Variant typed-leaf
predicates no longer prune row groups or pages.
`test_iceberg_variant_read` takes the test updates from #68137 and adds
the error barrier case from master.
The partial-Variant write check also refreshes the table in Spark before
reading it back. Spark caches Iceberg snapshots, so it could still
report the pre-insert row count.
### Release note
Fix Iceberg Variant queries returning a result instead of an error when
a Variant predicate follows an error-raising expression such as
`assert_true`.
### Check List (For Author)
- Test
- [x] Regression test
- [x] 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.
- [x] Yes. Variant typed-leaf predicates no longer prune Parquet row
groups or pages, as on master.
- Does this need documentation?
- [x] No.
- [ ] Yes.
### Check List (For Reviewer who merge this PR)
- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label
---
be/src/format_v2/parquet/parquet_statistics.cpp | 15 ++++-
.../format_v2/parquet/parquet_statistics_test.cpp | 21 ++++++
.../iceberg/test_iceberg_variant_read.groovy | 76 +++++++++++++---------
3 files changed, 79 insertions(+), 33 deletions(-)
diff --git a/be/src/format_v2/parquet/parquet_statistics.cpp
b/be/src/format_v2/parquet/parquet_statistics.cpp
index 33db2a7b558..002288c402c 100644
--- a/be/src/format_v2/parquet/parquet_statistics.cpp
+++ b/be/src/format_v2/parquet/parquet_statistics.cpp
@@ -640,8 +640,15 @@ std::optional<VariantShreddedPredicate>
extract_variant_shredded_predicate(
.op = *op};
}
+VExprContextSPtrs metadata_pruning_conjuncts(const format::FileScanRequest&
request) {
+ const size_t safe_count =
+ std::min(request.metadata_pruning_safe_conjunct_count,
request.conjuncts.size());
+ return VExprContextSPtrs(request.conjuncts.begin(),
request.conjuncts.begin() + safe_count);
+}
+
bool has_variant_shredded_filter(const format::FileScanRequest& request) {
- return std::ranges::any_of(request.conjuncts, [](const auto& conjunct) {
+ const auto conjuncts = metadata_pruning_conjuncts(request);
+ return std::ranges::any_of(conjuncts, [](const auto& conjunct) {
return extract_variant_shredded_predicate(conjunct).has_value();
});
}
@@ -1078,7 +1085,9 @@ bool check_shredded_variant_statistics(
const tparquet::FileMetaData& metadata, const tparquet::RowGroup&
row_group,
const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
const format::FileScanRequest& request, const cctz::time_zone*
timezone) {
- for (const auto& conjunct : request.conjuncts) {
+ // A Variant predicate localized after an unsafe conjunct must not skip
the row group before
+ // that earlier conjunct reaches its row-level evaluation.
+ for (const auto& conjunct : metadata_pruning_conjuncts(request)) {
const auto predicate = extract_variant_shredded_predicate(conjunct);
if (!predicate.has_value()) {
continue;
@@ -2060,7 +2069,7 @@ Status select_row_group_ranges_by_native_page_index(
}
}
- for (const auto& conjunct : request.conjuncts) {
+ for (const auto& conjunct : metadata_pruning_conjuncts(request)) {
const auto predicate = extract_variant_shredded_predicate(conjunct);
if (!predicate.has_value()) {
continue;
diff --git a/be/test/format_v2/parquet/parquet_statistics_test.cpp
b/be/test/format_v2/parquet/parquet_statistics_test.cpp
index d999c11fd0d..6f51499bc33 100644
--- a/be/test/format_v2/parquet/parquet_statistics_test.cpp
+++ b/be/test/format_v2/parquet/parquet_statistics_test.cpp
@@ -1942,6 +1942,17 @@ TEST(NativeParquetStatisticsTest,
ShreddedVariantTypedValueDrivesPageFiltering)
.ok());
EXPECT_TRUE(selected_row_groups.empty());
+ // The same predicate can be localized after an earlier unsafe conjunct.
Metadata pruning must
+ // preserve that earlier expression's row-level error instead of skipping
the whole row group.
+ request.metadata_pruning_safe_conjunct_count = 0;
+ ASSERT_TRUE(format::parquet::select_row_groups_by_metadata(
+ footer_only_metadata, schema, request, nullptr,
&selected_row_groups, false,
+ nullptr, nullptr, nullptr, nullptr, {},
+ format::parquet::ParquetMetadataProbeMode::FOOTER_ONLY)
+ .ok());
+ EXPECT_EQ(selected_row_groups, std::vector<int>({0}));
+ request.metadata_pruning_safe_conjunct_count =
std::numeric_limits<size_t>::max();
+
auto leaf_projection = format::LocalColumnIndex::partial_local(0);
auto typed_object_projection = format::LocalColumnIndex::partial_local(2);
auto field_projection = format::LocalColumnIndex::partial_local(0);
@@ -2054,6 +2065,16 @@ TEST(NativeParquetStatisticsTest,
ShreddedVariantTypedValueDrivesPageFiltering)
EXPECT_EQ(pruning_stats.page_index_read_calls, 1);
EXPECT_EQ(pruning_stats.filtered_page_rows, 50);
+ request.metadata_pruning_safe_conjunct_count = 0;
+ ASSERT_TRUE(format::parquet::select_row_group_ranges_by_native_page_index(
+ metadata, metadata.row_groups[0], page_indexes,
schema, request, 100,
+ &selected_ranges, &skip_plans, nullptr)
+ .ok());
+ ASSERT_EQ(selected_ranges.size(), 1);
+ EXPECT_EQ(selected_ranges[0].start, 0);
+ EXPECT_EQ(selected_ranges[0].length, 100);
+ request.metadata_pruning_safe_conjunct_count =
std::numeric_limits<size_t>::max();
+
auto row_group_with_root_residual = metadata.row_groups[0];
row_group_with_root_residual.columns[1].meta_data.statistics.__set_null_count(99);
ASSERT_TRUE(format::parquet::select_row_group_ranges_by_native_page_index(
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 eb723df128f..bbc0d3977a1 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
@@ -1225,12 +1225,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")
@@ -1398,8 +1406,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 """
@@ -1431,11 +1439,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)
@@ -1489,14 +1497,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 =
@@ -1507,9 +1513,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
@@ -1583,7 +1589,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)
@@ -1592,16 +1599,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,
@@ -1623,6 +1628,17 @@ public class AppendVariantEqualityDelete {
WHERE CAST(v['n'] AS INT) > 3000
"""
+ // A later Variant metadata predicate must not prune away an earlier
error-producing conjunct.
+ test {
+ sql """
+ SELECT COUNT(*)
+ FROM variant_page_pruning
+ WHERE assert_true(id != 1, 'variant_metadata_error_barrier')
+ AND v['n'] > 5000
+ """
+ exception "variant_metadata_error_barrier"
+ }
+
order_qt_variant_aggregate """
SELECT CAST(v['ok'] AS BOOLEAN),
COUNT(*),
@@ -1761,6 +1777,8 @@ public class AppendVariantEqualityDelete {
SELECT id
FROM variant_write_guard FOR VERSION AS OF ${writeGuardSourceSnapshot}
"""
+ // Spark caches Iceberg snapshots, so refresh after Doris commits before
cross-engine reads.
+ spark_iceberg """REFRESH TABLE demo.${dbName}.variant_write_guard"""
List<List<Object>> sparkPartialVariantRows = spark_iceberg """
SELECT COUNT(*), COUNT(payload)
FROM demo.${dbName}.variant_write_guard
@@ -1797,11 +1815,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")
// A STRING path must build a physical leaf projection rather than falling
back to rebuilding
// the complete Variant. Reading only the accessed leaves is the entire
advantage shredded
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]