github-actions[bot] commented on code in PR #65674:
URL: https://github.com/apache/doris/pull/65674#discussion_r3618732887
##########
be/src/format_v2/parquet/parquet_statistics.cpp:
##########
@@ -1388,127 +1051,148 @@ void collect_request_leaf_schemas(
}
}
-bool build_page_skip_plan_for_leaf(
- const std::shared_ptr<::parquet::RowGroupPageIndexReader>& row_group,
- const ParquetColumnSchema& column_schema, const std::vector<RowRange>&
selected_ranges,
- int64_t row_group_rows, ParquetPageSkipPlan* page_skip_plan) {
- DORIS_CHECK(page_skip_plan != nullptr);
- *page_skip_plan = ParquetPageSkipPlan {};
- if (column_schema.kind != ParquetColumnSchemaKind::PRIMITIVE ||
- column_schema.descriptor == nullptr || column_schema.leaf_column_id <
0 ||
- column_schema.descriptor->max_repetition_level() != 0) {
+template <typename ValueType>
+bool set_native_page_scalar_min_max(const tparquet::ColumnIndex& column_index,
+ const ParquetColumnSchema& column_schema,
size_t page_idx,
+ DecodedValueKind kind,
ParquetColumnStatistics* page_statistics,
+ const cctz::time_zone* timezone) {
+ if (page_idx >= column_index.min_values.size() || page_idx >=
column_index.max_values.size() ||
+ column_index.min_values[page_idx].size() != sizeof(ValueType) ||
+ column_index.max_values[page_idx].size() != sizeof(ValueType)) {
return false;
}
-
- std::shared_ptr<::parquet::OffsetIndex> offset_index;
- try {
- offset_index = row_group->GetOffsetIndex(column_schema.leaf_column_id);
- } catch (const ::parquet::ParquetException&) {
+ const auto min_value =
unaligned_load<ValueType>(column_index.min_values[page_idx].data());
+ const auto max_value =
unaligned_load<ValueType>(column_index.max_values[page_idx].data());
+ if constexpr (std::is_same_v<ValueType, int64_t>) {
+ if (!timestamp_min_max_is_safe(column_schema, min_value, max_value,
timezone)) {
+ return false;
+ }
+ }
+ if (!valid_min_max(min_value, max_value)) {
+ return true;
+ }
+ if (!set_decoded_field(column_schema, kind, min_value,
&page_statistics->min_value, timezone) ||
+ !set_decoded_field(column_schema, kind, max_value,
&page_statistics->max_value, timezone)) {
return false;
- } catch (const std::exception&) {
+ }
+ if (decoded_min_max_is_ordered(*page_statistics)) {
+ page_statistics->has_min_max = true;
+ }
+ return true;
+}
+
+bool build_native_page_statistics(const tparquet::ColumnIndex& column_index,
+ const ParquetColumnSchema& column_schema,
size_t page_idx,
+ ParquetColumnStatistics* page_statistics,
+ const cctz::time_zone* timezone) {
+ DORIS_CHECK(page_statistics != nullptr);
+ *page_statistics = {};
+ if (!column_index.__isset.null_counts || page_idx >=
column_index.null_pages.size() ||
+ page_idx >= column_index.null_counts.size()) {
return false;
}
- if (offset_index == nullptr) {
+ const int64_t null_count = column_index.null_counts[page_idx];
+ if (null_count < 0 || (column_index.null_pages[page_idx] && null_count ==
0)) {
Review Comment:
[P1] Validate null_count against this page's OffsetIndex row span before
using null_pages as an all-null proof. Page-index pruning only reaches flat
primitive leaves, so a ten-row page cannot legitimately report null_pages=true
with null_count=5 or 11; both currently set has_not_null=false, and an ordinary
comparison then returns kNoMatch and skips any real matching values without
reading the page. Bound every null count by the span and require an all-null
page's count to equal it, falling back from contradictory optional index
metadata.
##########
be/src/format_v2/parquet/parquet_scan.cpp:
##########
@@ -1585,6 +2120,10 @@ Status ParquetScanScheduler::read_next_batch(
_current_range_rows_read = 0;
}
if (*rows == 0) {
+ // Output-width feedback has no sample for a fully rejected batch.
Grow only the
+ // predicate-side physical read so a 32-row probe cannot pin a
long filtered prefix;
+ // any eventual output still stays below RuntimeState's row cap.
+ predicate_batch_rows =
grow_empty_predicate_batch(predicate_batch_rows);
Review Comment:
[P1] Keep the empty-prefix accelerator from widening the output batch.
FileScannerV2 passes a row cap derived from the preferred block bytes (32 for a
sufficiently wide projection), but after one rejected probe this grows the next
physical batch to 256. If those rows survive, read_current_row_group_batch
materializes and returns all 256; after further empty spans it can return 4096
rows, 128x the caller cap, so wide lazy columns can produce a multi-gigabyte
block or OOM. Please separate predicate-only probing from output
materialization, or otherwise ensure every returned/materialized batch remains
capped by _batch_size.
##########
be/src/format_v2/parquet/reader/native/page_reader.cpp:
##########
@@ -0,0 +1,306 @@
+// 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 "format_v2/parquet/reader/native/page_reader.h"
+
+#include <fmt/format.h>
+#include <gen_cpp/parquet_types.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#include <algorithm>
+
+#include "common/compiler_util.h" // IWYU pragma: keep
+#include "common/config.h"
+#include "io/fs/buffered_reader.h"
+#include "runtime/runtime_profile.h"
+#include "storage/cache/page_cache.h"
+#include "util/slice.h"
+#include "util/thrift_util.h"
+
+namespace doris {
+namespace io {
+struct IOContext;
+} // namespace io
+} // namespace doris
+
+namespace doris::format::parquet::native {
+static constexpr size_t INIT_PAGE_HEADER_SIZE = 128;
+
+template <bool IN_COLLECTION, bool OFFSET_INDEX>
+Status PageReader<IN_COLLECTION, OFFSET_INDEX>::_validate_page_header(uint32_t
header_size) const {
+ if (UNLIKELY(_cur_page_header.compressed_page_size < 0 ||
+ _cur_page_header.uncompressed_page_size < 0)) {
+ return Status::Corruption("Parquet page has a negative compressed or
uncompressed size");
+ }
+ if (UNLIKELY(header_size > _end_offset - _offset ||
Review Comment:
[P1] Reconcile each indexed PageLocation with the data page actually parsed
here. validate_offset_index only proves that the declared location rectangles
do not overlap, while this check only proves that the parsed header plus
payload fits somewhere in the whole chunk. A page can therefore cross the next
location; alternatively, this reader can skip an auxiliary page at the declared
offset and accept a shifted data page before next_page seeks backward into it.
Require both the parsed data page's header start to equal PageLocation.offset
and its serialized header plus payload size to equal
PageLocation.compressed_page_size (including on cache hits), or discard the
optional index and fall back.
##########
be/src/format_v2/parquet/native_schema_desc.cpp:
##########
@@ -0,0 +1,825 @@
+// 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 "format_v2/parquet/native_schema_desc.h"
+
+#include <ctype.h>
+
+#include <algorithm>
+#include <ostream>
+#include <utility>
+
+#include "common/cast_set.h"
+#include "common/exception.h"
+#include "common/logging.h"
+#include "core/data_type/data_type_array.h"
+#include "core/data_type/data_type_factory.hpp"
+#include "core/data_type/data_type_map.h"
+#include "core/data_type/data_type_struct.h"
+#include "core/data_type/define_primitive_type.h"
+#include "util/slice.h"
+#include "util/string_util.h"
+
+namespace doris::format::parquet {
+
+static bool is_group_node(const tparquet::SchemaElement& schema) {
+ return schema.num_children > 0;
+}
+
+static bool is_list_node(const tparquet::SchemaElement& schema) {
+ return schema.__isset.converted_type && schema.converted_type ==
tparquet::ConvertedType::LIST;
+}
+
+static bool is_map_node(const tparquet::SchemaElement& schema) {
+ return schema.__isset.converted_type &&
+ (schema.converted_type == tparquet::ConvertedType::MAP ||
+ schema.converted_type == tparquet::ConvertedType::MAP_KEY_VALUE);
+}
+
+static bool is_repeated_node(const tparquet::SchemaElement& schema) {
+ return schema.__isset.repetition_type &&
+ schema.repetition_type == tparquet::FieldRepetitionType::REPEATED;
+}
+
+static bool is_required_node(const tparquet::SchemaElement& schema) {
+ return schema.__isset.repetition_type &&
+ schema.repetition_type == tparquet::FieldRepetitionType::REQUIRED;
+}
+
+static bool is_optional_node(const tparquet::SchemaElement& schema) {
+ return schema.__isset.repetition_type &&
+ schema.repetition_type == tparquet::FieldRepetitionType::OPTIONAL;
+}
+
+static int num_children_node(const tparquet::SchemaElement& schema) {
+ return schema.__isset.num_children ? schema.num_children : 0;
+}
+
+static Status validate_native_schema_structure(
+ const std::vector<tparquet::SchemaElement>& schemas) {
+ if (schemas.empty()) {
+ return Status::InvalidArgument("Wrong parquet root schema element");
+ }
+
+ const auto& root = schemas[0];
+ if (root.__isset.type || !root.__isset.num_children || root.num_children
<= 0 ||
Review Comment:
[P2] Preserve the previous empty-schema contract. The replaced schema path
had an explicit positive test for a required root with zero children, and the
Parquet SchemaElement contract does not require num_children to be positive.
The new downstream path can already represent zero physical fields and answer
metadata-only COUNT(*), but this condition now rejects the file before reaching
it. Please allow num_children == 0 when the flattened schema contains only the
root and restore the compatibility test.
##########
be/src/format_v2/parquet/reader/native/level_reader.cpp:
##########
@@ -0,0 +1,216 @@
+// 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 "format_v2/parquet/reader/native/level_reader.h"
+
+#include <algorithm>
+#include <utility>
+
+#include "format_v2/parquet/native_schema_desc.h"
+#include "format_v2/parquet/reader/native/column_chunk_reader.h"
+#include "io/fs/buffered_reader.h"
+#include "io/fs/tracing_file_reader.h"
+
+namespace doris::format::parquet::native {
+
+class LevelReader::Impl {
+public:
+ virtual ~Impl() = default;
+
+ virtual Status init() = 0;
+ virtual Status read_rows(size_t rows, std::vector<level_t>*
repetition_levels,
+ std::vector<level_t>* definition_levels, size_t*
rows_read) = 0;
+ virtual ColumnChunkReaderStatistics statistics() = 0;
+};
+
+template <bool IN_COLLECTION>
+class LevelReaderImpl final : public LevelReader::Impl {
+public:
+ LevelReaderImpl(io::FileReaderSPtr file, tparquet::ColumnChunk
column_chunk,
+ NativeFieldSchema* field, size_t total_rows, size_t
max_buffer_size,
+ io::IOContext* io_ctx, bool enable_page_cache, std::string
page_cache_file_key,
+ ParquetReaderCompat compat)
+ : _file(std::move(file)),
+ _column_chunk(std::move(column_chunk)),
+ _field(field),
+ _total_rows(total_rows),
+ _max_buffer_size(max_buffer_size),
+ _io_ctx(io_ctx),
+ _enable_page_cache(enable_page_cache),
+ _page_cache_file_key(std::move(page_cache_file_key)),
+ _compat(compat) {}
+
+ Status init() override {
+ DORIS_CHECK(_file != nullptr);
+ DORIS_CHECK(_field != nullptr);
+ const auto& metadata = _column_chunk.meta_data;
+ ColumnChunkRange chunk_range;
+ RETURN_IF_ERROR(compute_column_chunk_range(metadata, _file->size(),
+
_compat.parquet_816_padding, &chunk_range));
+ const size_t chunk_start = chunk_range.offset;
+ const size_t chunk_size = chunk_range.length;
+ size_t prefetch_buffer_size = std::min(chunk_size, _max_buffer_size);
+ auto* tracing_reader =
typeid_cast<io::TracingFileReader*>(_file.get());
+ if ((tracing_reader != nullptr &&
+
typeid_cast<io::MergeRangeFileReader*>(tracing_reader->inner_reader().get()) !=
+ nullptr) ||
+ typeid_cast<io::MergeRangeFileReader*>(_file.get()) != nullptr) {
+ prefetch_buffer_size = 0;
+ }
+ _stream = std::make_unique<io::BufferedFileStreamReader>(_file,
chunk_start, chunk_size,
+
prefetch_buffer_size);
+ _chunk_reader = std::make_unique<ColumnChunkReader<IN_COLLECTION,
false>>(
+ _stream.get(), &_column_chunk, _field, nullptr, _total_rows,
_io_ctx,
+ ParquetPageReadContext(_enable_page_cache,
_page_cache_file_key,
+ _compat.data_page_v2_always_compressed),
+ &chunk_range);
+ return _chunk_reader->init();
+ }
+
+ Status read_rows(size_t rows, std::vector<level_t>* repetition_levels,
+ std::vector<level_t>* definition_levels, size_t*
rows_read) override {
+ DORIS_CHECK(repetition_levels != nullptr);
+ DORIS_CHECK(definition_levels != nullptr);
+ DORIS_CHECK(rows_read != nullptr);
+ repetition_levels->clear();
+ definition_levels->clear();
+ *rows_read = 0;
+ if (_current_row > _total_rows || rows > _total_rows - _current_row) {
+ return Status::Corruption("Parquet level reader requested rows
[{}, {}) of {}",
+ _current_row, _current_row + rows,
_total_rows);
+ }
+ if constexpr (IN_COLLECTION) {
+ RETURN_IF_ERROR(
+ read_nested_rows(rows, repetition_levels,
definition_levels, rows_read));
+ } else {
+ RETURN_IF_ERROR(read_flat_rows(rows, repetition_levels,
definition_levels, rows_read));
+ }
+ _current_row += *rows_read;
+ return Status::OK();
+ }
+
+ ColumnChunkReaderStatistics statistics() override { return
_chunk_reader->statistics(); }
+
+private:
+ Status read_flat_rows(size_t rows, std::vector<level_t>* repetition_levels,
+ std::vector<level_t>* definition_levels, size_t*
rows_read) {
+ while (*rows_read < rows) {
+ RETURN_IF_ERROR(_chunk_reader->parse_page_header());
+ RETURN_IF_ERROR(_chunk_reader->load_page_data_idempotent());
+ const size_t read_now = std::min(
+ rows - *rows_read,
static_cast<size_t>(_chunk_reader->remaining_num_values()));
+ if (read_now == 0) {
Review Comment:
[P2] Advance a zero-value data page before applying the no-progress error. A
leading V1/V2 page with num_values=0 and a later nonempty page is accepted by
the page/chunk validation and by the ordinary scalar reader, which observes an
empty row range and calls next_page. COUNT(nullable_col) reaches this
level-only path instead and returns corruption before its identical next-page
branch. Please skip bounded zero-value pages when another page exists and add
scalar-versus-COUNT parity coverage.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]