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 e8d638add56 [fix](parquet) Include file and Variant field context in
footer errors (#69092)
e8d638add56 is described below
commit e8d638add5617196128dd803cb05cb11cac2aa73
Author: Gabriel <[email protected]>
AuthorDate: Thu Oct 15 14:32:22 2026 +0800
[fix](parquet) Include file and Variant field context in footer errors
(#69092)
### What problem does this PR solve?
Parquet V2 footer/schema errors can reach the client without identifying
the input file. A Variant fallback error also reports only a wrapper
name such as `element`, which does not identify the field or explain
which part of its declaration is invalid.
Attach the source file path to errors returned while reading/parsing the
native footer, preserving the original status code and message. For
invalid Variant fallback values, also report the path relative to the
Variant root, declared physical type, repetition, and child count.
Diagnostic paths are constructed only on failure; repeated primitive
declarations are recovered from the normalized array child.
Example diagnostic:
```text
Failed to read Parquet footer for 's3://example-bucket/nested.parquet':
Parquet Variant wrapper element value must be an optional BYTE_ARRAY;
variant_path=payload.typed_value.element.value, physical_type=BYTE_ARRAY,
repetition=REQUIRED, children=0
```
The accepted file layouts and read semantics are unchanged.
### Validation
- Confirmed the three new diagnostic tests fail against the original
implementation.
- Rebuilt the changed translation units and schema tests in a focused
ASAN test binary: all 45 `ParquetSchemaTest.*` tests passed.
- Coverage includes required/repeated fallback leaves, incorrect
physical types, group fallback values, a valid control, unsupported
Variant versions, and truncated footers.
- clang-format 16.0.6 verification on all affected C++ files and `git
diff --check` passed.
### 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
- Behavior changed:
- [x] Yes. Error diagnostics include file and schema context; 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 | 118 ++++++++++++++++++++++
3 files changed, 164 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 434e61efda6..76181b6ac94 100644
--- a/be/src/format_v2/parquet/native_schema_desc.cpp
+++ b/be/src/format_v2/parquet/native_schema_desc.cpp
@@ -422,9 +422,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 8497e5ba529..ca746315eb8 100644
--- a/be/src/format_v2/parquet/parquet_file_context.cpp
+++ b/be/src/format_v2/parquet/parquet_file_context.cpp
@@ -339,9 +339,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));
+ enable_mapping_varbinary, enable_mapping_timestamp_tz);
+ 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 f7f96ea2c7e..53fe674eb54 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,117 @@ 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) {
+ auto schema = shredded_array_variant_schema(true, false);
+ 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]