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

yiguolei pushed a commit to branch branch-4.2
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/branch-4.2 by this push:
     new 388e30e9249 [fix](parquet) Include file and Variant field context in 
footer errors (#69119)
388e30e9249 is described below

commit 388e30e9249ee552db1e56e9c67230916abd1fbd
Author: Gabriel <[email protected]>
AuthorDate: Sun Oct 18 19:10:01 2026 +0800

    [fix](parquet) Include file and Variant field context in footer errors 
(#69119)
    
    ### What problem does this PR solve?
    
    Related PRs: #69092, #69094
    
    Port the Parquet V2 diagnostic improvement to branch-4.2. Footer/schema
    failures currently omit the input file path, and Variant fallback errors
    identify only a wrapper name such as `element`.
    
    Attach the file path while preserving the original status code and
    message. Invalid Variant fallback errors also include the
    Variant-relative field path, declared physical type, repetition, and
    child count. Diagnostic paths are constructed only on failure; accepted
    layouts and read semantics remain unchanged.
    
    ### Validation
    
    - Rebased onto `branch-4.2` at `9ffd818bec3`. Resolved the footer parser
    call conflict by preserving the new `preserve_binary_uuid` argument and
    the diagnostic error context. `git range-diff` confirms this argument is
    the only adaptation to the original patch; the follow-up Variant fixture
    correction is unchanged.
    - clang-format 16.0.6 verification on all affected files and `git diff
    --check` passed.
    - The source PR passed all 45 `ParquetSchemaTest.*` tests in a focused
    ASAN binary on branch-4.1. No new local build or unit-test run was
    performed for this branch-4.2 rebase; branch validation is pending CI.
    - Includes three diagnostic tests covering required/repeated fallback
    leaves, incorrect physical types, group fallback values, a valid
    control, unsupported Variant versions, and truncated footers.
    
    ### Release note
    
    Parquet V2 footer errors now identify the input file. Invalid Variant
    fallback errors additionally describe the field path and actual schema
    declaration.
    
    ### Check List (For Author)
    
    - Test
        - [x] Unit Test (ported; branch-4.2 execution pending CI)
    - Behavior changed:
    - [x] Yes. Diagnostic context is added; status codes and validation
    rules are preserved.
    - Does this need documentation?
        - [x] No.
    
    ### 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/native_schema_desc.cpp   |  40 +++++++-
 be/src/format_v2/parquet/parquet_file_context.cpp |  10 +-
 be/test/format_v2/parquet/parquet_schema_test.cpp | 119 ++++++++++++++++++++++
 3 files changed, 165 insertions(+), 4 deletions(-)

diff --git a/be/src/format_v2/parquet/native_schema_desc.cpp 
b/be/src/format_v2/parquet/native_schema_desc.cpp
index 6a21ce165cd..ef6ec867c6c 100644
--- a/be/src/format_v2/parquet/native_schema_desc.cpp
+++ b/be/src/format_v2/parquet/native_schema_desc.cpp
@@ -419,9 +419,45 @@ Status validate_variant_layout(const NativeFieldSchema& 
group_field,
         if (fallback != nullptr &&
             (!fallback->children.empty() || fallback->physical_type != 
tparquet::Type::BYTE_ARRAY ||
              !valid_fallback_repetition)) {
+            // Resolve the Variant-relative path only on failure; valid 
schemas need no extra
+            // per-node path allocation just to distinguish repeated wrapper 
names such as element.
+            std::string path;
+            std::function<bool(const NativeFieldSchema&)> find_path =
+                    [&](const NativeFieldSchema& field) {
+                        const auto prefix_size = path.size();
+                        if (!path.empty()) {
+                            path += '.';
+                        }
+                        path += field.name;
+                        if (&field == fallback) {
+                            return true;
+                        }
+                        for (const auto& child : field.children) {
+                            if (find_path(child)) {
+                                return true;
+                            }
+                        }
+                        path.resize(prefix_size);
+                        return false;
+                    };
+            find_path(group_field);
+            const auto* physical = &fallback->parquet_schema;
+            // A repeated primitive is normalized into an array whose child 
retains the original
+            // SchemaElement. Report that declaration rather than the 
synthetic array wrapper.
+            if (!physical->__isset.repetition_type && 
fallback->children.size() == 1 &&
+                fallback->children[0].parquet_schema.repetition_type ==
+                        tparquet::FieldRepetitionType::REPEATED) {
+                physical = &fallback->children[0].parquet_schema;
+            }
             return Status::Corruption(
-                    "Parquet Variant wrapper {} value must be an {} 
BYTE_ARRAY", wrapper.name,
-                    allow_required_fallback ? "optional or required" : 
"optional");
+                    "Parquet Variant wrapper {} value must be an {} 
BYTE_ARRAY; "
+                    "variant_path={}, physical_type={}, repetition={}, 
children={}",
+                    wrapper.name, allow_required_fallback ? "optional or 
required" : "optional",
+                    path, physical->__isset.type ? 
tparquet::to_string(physical->type) : "GROUP",
+                    physical->__isset.repetition_type
+                            ? tparquet::to_string(physical->repetition_type)
+                            : "UNSET",
+                    physical->__isset.num_children ? physical->num_children : 
0);
         }
         if (typed != nullptr) {
             if (!typed->parquet_schema.__isset.repetition_type ||
diff --git a/be/src/format_v2/parquet/parquet_file_context.cpp 
b/be/src/format_v2/parquet/parquet_file_context.cpp
index 72151b9aac0..6a89b363efd 100644
--- a/be/src/format_v2/parquet/parquet_file_context.cpp
+++ b/be/src/format_v2/parquet/parquet_file_context.cpp
@@ -344,9 +344,15 @@ Status ParquetFileContext::open(io::FileReaderSPtr 
input_file_reader, io::IOCont
             ++native_footer_cache_hits;
         } else {
             size_t native_footer_size = 0;
-            RETURN_IF_ERROR(parse_native_parquet_footer(
+            auto footer_status = parse_native_parquet_footer(
                     native_file, &loaded->metadata_owner, &native_footer_size, 
io_ctx,
-                    enable_mapping_varbinary, enable_mapping_timestamp_tz, 
preserve_binary_uuid));
+                    enable_mapping_varbinary, enable_mapping_timestamp_tz, 
preserve_binary_uuid);
+            if (!footer_status.ok()) {
+                // Schema validators have no file identity. Attach it before 
leaving the file
+                // layer, preserving the original code used by callers to 
classify the failure.
+                return footer_status.prepend(fmt::format("Failed to read 
Parquet footer for '{}': ",
+                                                         
native_file->path().native()));
+            }
             ++native_footer_read_calls;
             if (has_stable_meta_cache_identity && meta_cache != nullptr && 
meta_cache->enabled()) {
                 meta_cache->insert(meta_cache_key, 
loaded->metadata_owner.release(),
diff --git a/be/test/format_v2/parquet/parquet_schema_test.cpp 
b/be/test/format_v2/parquet/parquet_schema_test.cpp
index 97a10c3bd69..f51462de177 100644
--- a/be/test/format_v2/parquet/parquet_schema_test.cpp
+++ b/be/test/format_v2/parquet/parquet_schema_test.cpp
@@ -17,6 +17,9 @@
 
 #include <gtest/gtest.h>
 
+#include <algorithm>
+#include <array>
+#include <cstring>
 #include <limits>
 #include <optional>
 #include <string>
@@ -37,6 +40,10 @@
 #include "format_v2/parquet/parquet_column_schema.h"
 #include "format_v2/parquet/parquet_file_context.h"
 #include "format_v2/parquet/reader/native_column_reader.h"
+#include "io/fs/file_reader.h"
+#include "io/io_common.h"
+#include "util/coding.h"
+#include "util/thrift_util.h"
 
 namespace doris::format::parquet {
 
@@ -221,6 +228,118 @@ std::vector<tparquet::SchemaElement> 
shredded_array_variant_schema(bool include_
 
 } // namespace
 
+namespace {
+
+class FooterDiagnosticFileReader final : public io::FileReader {
+public:
+    explicit FooterDiagnosticFileReader(std::string bytes) : 
_bytes(std::move(bytes)) {}
+    Status close() override { return Status::OK(); }
+    const io::Path& path() const override { return _path; }
+    size_t size() const override { return _bytes.size(); }
+    bool closed() const override { return false; }
+    int64_t mtime() const override { return 0; }
+
+protected:
+    Status read_at_impl(size_t offset, Slice result, size_t* bytes_read,
+                        const io::IOContext*) override {
+        *bytes_read = offset < _bytes.size() ? std::min(result.size, 
_bytes.size() - offset) : 0;
+        if (*bytes_read != 0) {
+            memcpy(result.data, _bytes.data() + offset, *bytes_read);
+        }
+        return Status::OK();
+    }
+
+private:
+    io::Path _path {"s3://example-bucket/nested.parquet"};
+    std::string _bytes;
+};
+
+Status open_diagnostic_footer(const std::vector<tparquet::SchemaElement>& 
schema) {
+    tparquet::FileMetaData metadata;
+    metadata.__set_version(1);
+    metadata.__set_schema(schema);
+    metadata.__set_num_rows(0);
+    metadata.__set_row_groups({});
+    std::vector<uint8_t> footer;
+    ThriftSerializer serializer(true, 1024);
+    RETURN_IF_ERROR(serializer.serialize(&metadata, &footer));
+    std::string bytes = "PAR1";
+    bytes.append(reinterpret_cast<const char*>(footer.data()), footer.size());
+    std::array<uint8_t, 4> length;
+    encode_fixed32_le(length.data(), static_cast<uint32_t>(footer.size()));
+    bytes.append(reinterpret_cast<const char*>(length.data()), length.size());
+    bytes.append("PAR1");
+    auto file = std::make_shared<FooterDiagnosticFileReader>(std::move(bytes));
+    ParquetFileContext context;
+    return context.open(file, nullptr, false, io::FileDescription(), false, 
false, nullptr);
+}
+
+} // namespace
+
+TEST(ParquetSchemaTest, VariantFallbackErrorReportsPathAndActualSchema) {
+    // A typed carrier still needs an optional fallback; only fallback-only 
elements may require it.
+    auto schema = shredded_array_variant_schema(true, true);
+    schema[7].__set_repetition_type(tparquet::FieldRepetitionType::REQUIRED);
+    auto status = open_diagnostic_footer(schema);
+    ASSERT_TRUE(status.is<ErrorCode::CORRUPTION>()) << status;
+    EXPECT_NE(status.to_string().find("s3://example-bucket/nested.parquet"), 
std::string::npos);
+    
EXPECT_NE(status.to_string().find("variant_path=payload.typed_value.element.value"),
+              std::string::npos);
+    EXPECT_NE(status.to_string().find("physical_type=BYTE_ARRAY"), 
std::string::npos);
+    EXPECT_NE(status.to_string().find("repetition=REQUIRED"), 
std::string::npos);
+    EXPECT_NE(status.to_string().find("children=0"), std::string::npos);
+
+    schema[7].__set_repetition_type(tparquet::FieldRepetitionType::OPTIONAL);
+    ASSERT_TRUE(open_diagnostic_footer(schema).ok());
+    schema[7].__set_type(tparquet::Type::INT64);
+    status = open_diagnostic_footer(schema);
+    ASSERT_TRUE(status.is<ErrorCode::CORRUPTION>()) << status;
+    EXPECT_NE(status.to_string().find("physical_type=INT64"), 
std::string::npos);
+    EXPECT_NE(status.to_string().find("repetition=OPTIONAL"), 
std::string::npos);
+
+    schema[7].__set_type(tparquet::Type::BYTE_ARRAY);
+    schema[7].__set_repetition_type(tparquet::FieldRepetitionType::REPEATED);
+    status = open_diagnostic_footer(schema);
+    ASSERT_TRUE(status.is<ErrorCode::CORRUPTION>()) << status;
+    EXPECT_NE(status.to_string().find("physical_type=BYTE_ARRAY"), 
std::string::npos);
+    EXPECT_NE(status.to_string().find("repetition=REPEATED"), 
std::string::npos);
+}
+
+TEST(ParquetSchemaTest, VariantFallbackGroupErrorReportsActualChildren) {
+    auto schema = shredded_object_variant_schema();
+    auto child = schema[6];
+    child.__set_name("child");
+    schema[6].__isset.type = false;
+    schema[6].__set_num_children(1);
+    schema.insert(schema.begin() + 7, child);
+    const auto status = open_diagnostic_footer(schema);
+    ASSERT_TRUE(status.is<ErrorCode::CORRUPTION>()) << status;
+    
EXPECT_NE(status.to_string().find("variant_path=payload.typed_value.field.value"),
+              std::string::npos);
+    EXPECT_NE(status.to_string().find("physical_type=GROUP"), 
std::string::npos);
+    EXPECT_NE(status.to_string().find("children=1"), std::string::npos);
+}
+
+TEST(ParquetSchemaTest, FooterContextPreservesUnsupportedAndCorruptionCodes) {
+    auto schema = unshredded_variant_schema(2);
+    NativeFieldDescriptor descriptor;
+    const auto expected = descriptor.parse_from_thrift(schema);
+    ASSERT_FALSE(expected.ok());
+    const auto unsupported = open_diagnostic_footer(schema);
+    EXPECT_EQ(unsupported.code(), expected.code());
+    EXPECT_NE(unsupported.to_string().find(expected.msg()), std::string::npos);
+    
EXPECT_NE(unsupported.to_string().find("s3://example-bucket/nested.parquet"),
+              std::string::npos);
+
+    auto file = std::make_shared<FooterDiagnosticFileReader>("PAR1");
+    ParquetFileContext context;
+    const auto corrupt =
+            context.open(file, nullptr, false, io::FileDescription(), false, 
false, nullptr);
+    ASSERT_TRUE(corrupt.is<ErrorCode::CORRUPTION>()) << corrupt;
+    EXPECT_NE(corrupt.to_string().find("s3://example-bucket/nested.parquet"), 
std::string::npos);
+    EXPECT_NE(corrupt.to_string().find("too small for a footer"), 
std::string::npos);
+}
+
 TEST(ParquetSchemaTest, NativeSchemaRecognizesVariantLogicalGroup) {
     for (const auto version : {std::optional<int8_t> {}, std::optional<int8_t> 
{1}}) {
         NativeFieldDescriptor descriptor;


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

Reply via email to