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 896a722a60e branch-4.1: [fix](parquet) Restore column ownership after 
interrupted reads #66780 (#68840)
896a722a60e is described below

commit 896a722a60ecd465a605da927a121dba5b9d1586
Author: Mr.Vitvara Varavithya <[email protected]>
AuthorDate: Sat Oct 10 15:33:13 2026 +0700

    branch-4.1: [fix](parquet) Restore column ownership after interrupted reads 
#66780 (#68840)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: #66780
    
    Problem Summary:
    
    Cherry-pick of #66780 to branch-4.1. When a Parquet scalar column is
    read directly into the destination column
    (`read_directly_into_dst_logical_column()`), `read_column_data` moves
    the caller's only `ColumnPtr` into `resolved_column`; if any read step
    returns before `convert()` moves it back (for example when the scan is
    stopped), the caller is left with a null column and the BE crashes. The
    fix restores ownership with a `Defer` on every early return, exactly as
    on master.
    
    Conflict resolution: on branch-4.1 `read_column_data` ends with `return
    _converter->convert(...)` directly, so the last hunk becomes `return
    convert_column();` (master has `RETURN_IF_ERROR(convert_column()); …
    return Status::OK();`) — same behaviour.
    
    The unit test from #66780 is carried over as a standalone file,
    `parquet_scalar_column_reader_test.cpp`: the file it extends on master
    (`parquet_column_chunk_reader_test.cpp`) and the lazy-dictionary
    fixtures it builds on (#66000) are not on branch-4.1, so the test brings
    the minimal in-memory column-chunk fixture it needs.
    
    ### Release note
    
    Fix a BE crash when a Parquet read into a nested scalar column is
    interrupted before the column is converted.
    
    ### Check List (For Author)
    
    - Test
        - [ ] Regression test
    - [x] Unit Test:
    `ParquetScalarColumnReaderTest.NestedReadRestoresColumnWhenStopped`
    (`be/test/format/parquet/parquet_scalar_column_reader_test.cpp`)
        - [ ] 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:
        - [x] No.
        - [ ] Yes.
    
    - 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/parquet/vparquet_column_reader.cpp   |  22 ++-
 .../parquet/parquet_scalar_column_reader_test.cpp  | 170 +++++++++++++++++++++
 2 files changed, 188 insertions(+), 4 deletions(-)

diff --git a/be/src/format/parquet/vparquet_column_reader.cpp 
b/be/src/format/parquet/vparquet_column_reader.cpp
index 82a0b81bc85..7c9d57d2e14 100644
--- a/be/src/format/parquet/vparquet_column_reader.cpp
+++ b/be/src/format/parquet/vparquet_column_reader.cpp
@@ -41,6 +41,7 @@
 #include "format/table/iceberg_default_value.h"
 #include "io/fs/tracing_file_reader.h"
 #include "runtime/runtime_profile.h"
+#include "util/defer_op.h"
 
 namespace doris {
 #include "common/compile_check_begin.h"
@@ -548,11 +549,26 @@ Status ScalarColumnReader<IN_COLLECTION, 
OFFSET_INDEX>::read_column_data(
     ColumnPtr resolved_column =
             _converter->get_physical_column(_field_schema->physical_type, 
_field_schema->data_type,
                                             doris_column, type, 
is_dict_filter);
+    // Direct reads transfer the caller's only ColumnPtr so mutate() can avoid 
cloning it. Restore
+    // that ownership if any read step returns before convert() moves the 
column back.
+    bool restore_doris_column = false;
+    Defer restore_column([&]() {
+        if (restore_doris_column) {
+            doris_column = std::move(resolved_column);
+        }
+    });
     if (_converter->read_directly_into_dst_logical_column()) {
         DCHECK_EQ(resolved_column.get(), doris_column.get());
         resolved_column = std::move(doris_column);
+        restore_doris_column = true;
     }
     DataTypePtr& resolved_type = _converter->get_physical_type();
+    auto convert_column = [&]() -> Status {
+        RETURN_IF_ERROR(_converter->convert(resolved_column, 
_field_schema->data_type, type,
+                                            doris_column, is_dict_filter));
+        restore_doris_column = false;
+        return Status::OK();
+    };
 
     _def_levels.clear();
     _rep_levels.clear();
@@ -561,8 +577,7 @@ Status ScalarColumnReader<IN_COLLECTION, 
OFFSET_INDEX>::read_column_data(
     if (_in_nested) {
         RETURN_IF_ERROR(_read_nested_column(resolved_column, resolved_type, 
filter_map, batch_size,
                                             read_rows, eof, is_dict_filter));
-        return _converter->convert(resolved_column, _field_schema->data_type, 
type, doris_column,
-                                   is_dict_filter);
+        return convert_column();
     }
 
     int64_t right_row = 0;
@@ -638,8 +653,7 @@ Status ScalarColumnReader<IN_COLLECTION, 
OFFSET_INDEX>::read_column_data(
         }
     }
 
-    return _converter->convert(resolved_column, _field_schema->data_type, 
type, doris_column,
-                               is_dict_filter);
+    return convert_column();
 }
 
 Status ArrayColumnReader::init(std::unique_ptr<ParquetColumnReader> 
element_reader,
diff --git a/be/test/format/parquet/parquet_scalar_column_reader_test.cpp 
b/be/test/format/parquet/parquet_scalar_column_reader_test.cpp
new file mode 100644
index 00000000000..98e1d43999f
--- /dev/null
+++ b/be/test/format/parquet/parquet_scalar_column_reader_test.cpp
@@ -0,0 +1,170 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include <gtest/gtest.h>
+
+#include <algorithm>
+#include <cstdint>
+#include <cstring>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "common/cast_set.h"
+#include "common/status.h"
+#include "core/column/column_string.h"
+#include "core/data_type/data_type_string.h"
+#include "format/parquet/parquet_common.h"
+#include "format/parquet/schema_desc.h"
+#include "format/parquet/vparquet_column_reader.h"
+#include "io/fs/file_reader.h"
+#include "io/io_common.h"
+#include "runtime/runtime_state.h"
+#include "util/thrift_util.h"
+
+namespace doris {
+namespace {
+
+// Serves an in-memory byte buffer as the parquet file behind a column reader.
+class InMemoryFileReader final : public io::FileReader {
+public:
+    explicit InMemoryFileReader(std::vector<uint8_t> data) : 
_data(std::move(data)) {}
+
+    Status close() override {
+        _closed = true;
+        return Status::OK();
+    }
+
+    const io::Path& path() const override { return _path; }
+    size_t size() const override { return _data.size(); }
+    bool closed() const override { return _closed; }
+    int64_t mtime() const override { return 0; }
+
+protected:
+    Status read_at_impl(size_t offset, Slice result, size_t* bytes_read,
+                        const io::IOContext* io_ctx) override {
+        if (offset > _data.size()) {
+            return Status::IOError("Out of bounds");
+        }
+        *bytes_read = std::min(result.size, _data.size() - offset);
+        memcpy(result.data, _data.data() + offset, *bytes_read);
+        return Status::OK();
+    }
+
+private:
+    std::vector<uint8_t> _data;
+    io::Path _path = "parquet_scalar_column_reader_test";
+    bool _closed = false;
+};
+
+struct ColumnChunkFixture {
+    std::vector<uint8_t> data;
+    tparquet::ColumnChunk chunk;
+    FieldSchema field_schema;
+};
+
+Status append_page(tparquet::PageHeader* header, const std::vector<uint8_t>& 
payload,
+                   std::vector<uint8_t>& data, int64_t* page_offset, int32_t* 
page_size) {
+    std::vector<uint8_t> header_bytes;
+    ThriftSerializer serializer(/*compact=*/true, /*initial_buffer_size=*/256);
+    RETURN_IF_ERROR(serializer.serialize(header, &header_bytes));
+
+    *page_offset = data.size();
+    data.insert(data.end(), header_bytes.begin(), header_bytes.end());
+    data.insert(data.end(), payload.begin(), payload.end());
+    *page_size = cast_set<int32_t>(header_bytes.size() + payload.size());
+    return Status::OK();
+}
+
+tparquet::PageHeader make_data_page_header(tparquet::Encoding::type encoding) {
+    tparquet::DataPageHeader data_header;
+    data_header.__set_num_values(1);
+    data_header.__set_encoding(encoding);
+    data_header.__set_definition_level_encoding(tparquet::Encoding::RLE);
+    data_header.__set_repetition_level_encoding(tparquet::Encoding::RLE);
+
+    tparquet::PageHeader header;
+    header.type = tparquet::PageType::DATA_PAGE;
+    header.__set_compressed_page_size(1);
+    header.__set_uncompressed_page_size(1);
+    header.__set_data_page_header(data_header);
+    return header;
+}
+
+// One uncompressed BYTE_ARRAY column chunk holding a single plain-encoded 
data page.
+Status make_plain_fixture(ColumnChunkFixture* fixture) {
+    constexpr size_t PREFIX_SIZE = 16;
+    fixture->data.resize(PREFIX_SIZE, 0);
+
+    tparquet::PageHeader header = 
make_data_page_header(tparquet::Encoding::PLAIN);
+    int64_t data_page_offset = 0;
+    int32_t data_page_size = 0;
+    RETURN_IF_ERROR(append_page(&header, {0}, fixture->data, 
&data_page_offset, &data_page_size));
+
+    auto& metadata = fixture->chunk.meta_data;
+    metadata.__set_type(tparquet::Type::BYTE_ARRAY);
+    metadata.__set_codec(tparquet::CompressionCodec::UNCOMPRESSED);
+    metadata.__set_num_values(1);
+    metadata.__set_data_page_offset(data_page_offset);
+    metadata.__set_total_compressed_size(
+            cast_set<int64_t>(fixture->data.size() - data_page_offset));
+
+    fixture->field_schema.physical_type = tparquet::Type::BYTE_ARRAY;
+    return Status::OK();
+}
+
+} // namespace
+
+// A direct read moves the caller's column into the reader; a read that stops 
before convert()
+// hands it back, so the caller still holds a column instead of a null pointer.
+TEST(ParquetScalarColumnReaderTest, NestedReadRestoresColumnWhenStopped) {
+    ColumnChunkFixture fixture;
+    ASSERT_TRUE(make_plain_fixture(&fixture).ok());
+    auto file_reader = 
std::make_shared<InMemoryFileReader>(std::move(fixture.data));
+    DataTypePtr string_type = std::make_shared<DataTypeString>();
+    fixture.field_schema.data_type = string_type;
+    fixture.field_schema.parquet_schema.__set_type(tparquet::Type::BYTE_ARRAY);
+
+    RowRanges row_ranges;
+    row_ranges.add({0, 1});
+    io::IOContext io_ctx;
+    ScalarColumnReader<true, false> reader(row_ranges, 1, fixture.chunk, 
nullptr, nullptr, &io_ctx);
+    reader.set_column_in_nested();
+
+    TQueryOptions query_options;
+    query_options.__set_enable_parquet_file_page_cache(false);
+    RuntimeState runtime_state(query_options, TQueryGlobals());
+    ASSERT_TRUE(reader.init(file_reader, &fixture.field_schema,
+                            /*max_buf_size=*/1024 * 1024, &runtime_state)
+                        .ok());
+
+    ColumnPtr column = ColumnString::create();
+    FilterMap filter_map;
+    ASSERT_TRUE(filter_map.init(nullptr, 0, false).ok());
+    size_t read_rows = 0;
+    bool eof = false;
+    io_ctx.should_stop = true;
+
+    Status status = reader.read_column_data(column, string_type, nullptr, 
filter_map, 1, &read_rows,
+                                            &eof, false);
+    EXPECT_TRUE(status.is<ErrorCode::END_OF_FILE>()) << status;
+    ASSERT_TRUE(column);
+    EXPECT_TRUE(column->empty());
+}
+
+} // namespace doris


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

Reply via email to