github-actions[bot] commented on code in PR #65674:
URL: https://github.com/apache/doris/pull/65674#discussion_r3608716910
##########
be/src/format_v2/parquet/parquet_reader.cpp:
##########
@@ -119,22 +119,23 @@ void collect_request_leaf_column_ids(
}
std::vector<ParquetPageCacheRange> build_page_cache_ranges(
- const ::parquet::FileMetaData& metadata,
+ const tparquet::FileMetaData& metadata,
const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
const format::FileScanRequest& request, const RowGroupScanPlan&
row_group_plan) {
std::unordered_set<int> leaf_column_ids;
collect_request_leaf_column_ids(file_schema, request, &leaf_column_ids);
std::vector<ParquetPageCacheRange> ranges;
ranges.reserve(row_group_plan.row_groups.size() * leaf_column_ids.size());
for (const auto& row_group_plan_item : row_group_plan.row_groups) {
- auto row_group_metadata =
metadata.RowGroup(row_group_plan_item.row_group_id);
- DORIS_CHECK(row_group_metadata != nullptr);
+ const auto& row_group_metadata =
metadata.row_groups[row_group_plan_item.row_group_id];
for (const auto leaf_column_id : leaf_column_ids) {
- DORIS_CHECK(leaf_column_id >= 0 && leaf_column_id <
row_group_metadata->num_columns());
- auto column_metadata =
row_group_metadata->ColumnChunk(leaf_column_id);
- DORIS_CHECK(column_metadata != nullptr);
- const int64_t offset = column_chunk_start_offset(*column_metadata);
- const int64_t size = column_metadata->total_compressed_size();
+ DORIS_CHECK(leaf_column_id >= 0 &&
+ leaf_column_id <
static_cast<int>(row_group_metadata.columns.size()));
+ const auto& column_metadata =
row_group_metadata.columns[leaf_column_id].meta_data;
+ const int64_t offset =
column_metadata.__isset.dictionary_page_offset
Review Comment:
[P1] Remove or validate this obsolete page-cache range traversal The new
metadata-safety test deliberately permits an optional `dictionary_page_offset =
-1` by falling back to the valid data-page offset in
`build_native_prefetch_ranges()`. This separate open-time helper instead
selects the set dictionary offset unconditionally and `DORIS_CHECK`s it, so
enabling the Parquet page cache turns that same file into a BE abort.
`register_page_cache_ranges()` now discards the result because native readers
cache exact pages, so remove this traversal or reuse the checked chunk-range
helper and propagate a `Status` rather than asserting on file metadata.
##########
be/src/format_v2/parquet/native_schema_desc.cpp:
##########
@@ -0,0 +1,779 @@
+// 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() || !is_group_node(schemas[0])) {
+ return Status::InvalidArgument("Wrong parquet root schema element");
+ }
+
+ struct PendingGroup {
+ size_t remaining_children;
+ size_t depth;
+ };
+ const auto root_children = num_children_node(schemas[0]);
+ if (root_children < 0 || static_cast<size_t>(root_children) >
schemas.size() - 1) {
+ return Status::InvalidArgument("Invalid parquet root child count {}",
root_children);
+ }
+ std::vector<PendingGroup> pending {{static_cast<size_t>(root_children),
0}};
+ for (size_t pos = 1; pos < schemas.size(); ++pos) {
+ while (!pending.empty() && pending.back().remaining_children == 0) {
+ pending.pop_back();
+ }
+ if (pending.empty()) {
+ return Status::InvalidArgument("Schema element {} is not reachable
from the root", pos);
+ }
+
+ const size_t depth = pending.back().depth + 1;
+ --pending.back().remaining_children;
+ const int children = num_children_node(schemas[pos]);
+ if (children < 0 || static_cast<size_t>(children) > schemas.size() -
pos - 1) {
+ return Status::InvalidArgument("Invalid child count {} at schema
element {}", children,
+ pos);
+ }
+ if (children > 0) {
+ // Bound the tree before any resize or recursion so an untrusted
footer cannot
+ // turn a tiny schema into an unbounded allocation or parser stack.
+ if (depth > MAX_NATIVE_SCHEMA_DEPTH) {
+ return Status::InvalidArgument("Parquet schema depth {}
exceeds limit {}", depth,
+ MAX_NATIVE_SCHEMA_DEPTH);
+ }
+ pending.push_back({static_cast<size_t>(children), depth});
+ }
+ }
+ while (!pending.empty() && pending.back().remaining_children == 0) {
+ pending.pop_back();
+ }
+ if (!pending.empty()) {
+ return Status::InvalidArgument("Parquet schema ended before all
children were parsed");
+ }
+ return Status::OK();
+}
+
+/**
+ * `repeated_parent_def_level` is the definition level of the first ancestor
node whose repetition_type equals REPEATED.
+ * Empty array/map values are not stored in doris columns, so have to use
`repeated_parent_def_level` to skip the
+ * empty or null values in ancestor node.
+ *
+ * For instance, considering an array of strings with 3 rows like the
following:
+ * null, [], [a, b, c]
+ * We can store four elements in data column: null, a, b, c
+ * and the offsets column is: 1, 1, 4
+ * and the null map is: 1, 0, 0
+ * For the i-th row in array column: range from `offsets[i - 1]` until
`offsets[i]` represents the elements in this row,
+ * so we can't store empty array/map values in doris data column.
+ * As a comparison, spark does not require `repeated_parent_def_level`,
+ * because the spark column stores empty array/map values , and use anther
length column to indicate empty values.
+ * Please reference:
https://github.com/apache/spark/blob/master/sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/ParquetColumnVector.java
+ *
+ * Furthermore, we can also avoid store null array/map values in doris data
column.
+ * The same three rows as above, We can only store three elements in data
column: a, b, c
+ * and the offsets column is: 0, 0, 3
+ * and the null map is: 1, 0, 0
+ *
+ * Inherit the repetition and definition level from parent node, if the parent
node is repeated,
+ * we should set repeated_parent_def_level = definition_level, otherwise as
repeated_parent_def_level.
+ * @param parent parent node
+ * @param repeated_parent_def_level the first ancestor node whose
repetition_type equals REPEATED
+ */
+static void set_child_node_level(NativeFieldSchema* parent, int16_t
repeated_parent_def_level) {
+ for (auto& child : parent->children) {
+ child.repetition_level = parent->repetition_level;
+ child.definition_level = parent->definition_level;
+ child.repeated_parent_def_level = repeated_parent_def_level;
+ }
+}
+
+static bool is_struct_list_node(const tparquet::SchemaElement& schema) {
+ const std::string& name = schema.name;
+ static const Slice array_slice("array", 5);
+ static const Slice tuple_slice("_tuple", 6);
+ Slice slice(name);
+ return slice == array_slice || slice.ends_with(tuple_slice);
+}
+
+std::string NativeFieldSchema::debug_string() const {
+ std::stringstream ss;
+ ss << "NativeFieldSchema(name=" << name << ", R=" << repetition_level
+ << ", D=" << definition_level;
+ if (children.size() > 0) {
+ ss << ", type=" << data_type->get_name() << ", children=[";
+ for (int i = 0; i < children.size(); ++i) {
+ if (i != 0) {
+ ss << ", ";
+ }
+ ss << children[i].debug_string();
+ }
+ ss << "]";
+ } else {
+ ss << ", physical_type=" << physical_type;
+ ss << " , doris_type=" << data_type->get_name();
+ }
+ ss << ")";
+ return ss.str();
+}
+
+Status NativeFieldDescriptor::parse_from_thrift(
+ const std::vector<tparquet::SchemaElement>& t_schemas) {
+ _fields.clear();
+ _physical_fields.clear();
+ _name_to_field.clear();
+ RETURN_IF_ERROR(validate_native_schema_structure(t_schemas));
+ const auto& root_schema = t_schemas[0];
+ _fields.resize(root_schema.num_children);
+ _next_schema_pos = 1;
+
+ for (int i = 0; i < root_schema.num_children; ++i) {
+ RETURN_IF_ERROR(parse_node_field(t_schemas, _next_schema_pos,
&_fields[i]));
+ if (_name_to_field.find(_fields[i].name) != _name_to_field.end()) {
+ return Status::InvalidArgument("Duplicated field name: {}",
_fields[i].name);
+ }
+ _name_to_field.emplace(_fields[i].name, &_fields[i]);
+ }
+
+ if (_next_schema_pos != t_schemas.size()) {
+ return Status::InvalidArgument("Remaining {} unparsed schema elements",
+ t_schemas.size() - _next_schema_pos);
+ }
+
+ return Status::OK();
+}
+
+Status NativeFieldDescriptor::parse_node_field(
+ const std::vector<tparquet::SchemaElement>& t_schemas, size_t curr_pos,
+ NativeFieldSchema* node_field) {
+ if (curr_pos >= t_schemas.size()) {
+ return Status::InvalidArgument("Out-of-bounds index of schema
elements");
+ }
+ auto& t_schema = t_schemas[curr_pos];
+ if (is_group_node(t_schema)) {
+ // nested structure or nullable list
+ return parse_group_field(t_schemas, curr_pos, node_field);
+ }
+ if (is_repeated_node(t_schema)) {
+ // repeated <primitive-type> <name> (LIST)
+ // produce required list<element>
+ node_field->repetition_level++;
+ node_field->definition_level++;
+ node_field->children.resize(1);
+ set_child_node_level(node_field, node_field->definition_level);
+ auto child = &node_field->children[0];
+ parse_physical_field(t_schema, false, child);
+
+ node_field->name = t_schema.name;
+ node_field->lower_case_name = to_lower(t_schema.name);
+ node_field->data_type =
std::make_shared<DataTypeArray>(make_nullable(child->data_type));
+ _next_schema_pos = curr_pos + 1;
+ node_field->field_id = t_schema.__isset.field_id ? t_schema.field_id :
-1;
+ } else {
+ bool is_optional = is_optional_node(t_schema);
+ if (is_optional) {
+ node_field->definition_level++;
+ }
+ parse_physical_field(t_schema, is_optional, node_field);
+ _next_schema_pos = curr_pos + 1;
+ }
+ return Status::OK();
+}
+
+void NativeFieldDescriptor::parse_physical_field(const
tparquet::SchemaElement& physical_schema,
+ bool is_nullable,
+ NativeFieldSchema*
physical_field) {
+ physical_field->name = physical_schema.name;
+ physical_field->lower_case_name = to_lower(physical_field->name);
+ physical_field->parquet_schema = physical_schema;
+ physical_field->physical_type = physical_schema.type;
+ physical_field->column_id = NATIVE_UNASSIGNED_COLUMN_ID; // Initialize
column_id
+ _physical_fields.push_back(physical_field);
+ physical_field->physical_column_index =
cast_set<int>(_physical_fields.size() - 1);
+ auto type = get_doris_type(physical_schema, is_nullable);
+ physical_field->data_type = type.first;
+ physical_field->is_type_compatibility = type.second;
+ physical_field->field_id = physical_schema.__isset.field_id ?
physical_schema.field_id : -1;
+}
+
+std::pair<DataTypePtr, bool> NativeFieldDescriptor::get_doris_type(
+ const tparquet::SchemaElement& physical_schema, bool nullable) {
+ std::pair<DataTypePtr, bool> ans = {std::make_shared<DataTypeNothing>(),
false};
+ try {
+ if (physical_schema.__isset.logicalType) {
+ ans = convert_to_doris_type(physical_schema.logicalType, nullable);
+ } else if (physical_schema.__isset.converted_type) {
+ ans = convert_to_doris_type(physical_schema, nullable);
+ }
+ } catch (...) {
+ // now the Not supported exception are ignored
+ // so those byte_array maybe be treated as varbinary(now) :
string(before)
+ }
+ if (ans.first->get_primitive_type() == PrimitiveType::INVALID_TYPE) {
+ switch (physical_schema.type) {
+ case tparquet::Type::BOOLEAN:
+ ans.first =
DataTypeFactory::instance().create_data_type(TYPE_BOOLEAN, nullable);
+ break;
+ case tparquet::Type::INT32:
+ ans.first = DataTypeFactory::instance().create_data_type(TYPE_INT,
nullable);
+ break;
+ case tparquet::Type::INT64:
+ ans.first =
DataTypeFactory::instance().create_data_type(TYPE_BIGINT, nullable);
+ break;
+ case tparquet::Type::INT96:
+ if (_enable_mapping_timestamp_tz) {
+ // treat INT96 as TIMESTAMPTZ
+ ans.first =
DataTypeFactory::instance().create_data_type(TYPE_TIMESTAMPTZ, nullable,
+ 0, 6);
+ } else {
+ // in most cases, it's a nano timestamp
+ ans.first =
DataTypeFactory::instance().create_data_type(TYPE_DATETIMEV2, nullable,
+ 0, 6);
+ }
+ break;
+ case tparquet::Type::FLOAT:
+ ans.first =
DataTypeFactory::instance().create_data_type(TYPE_FLOAT, nullable);
+ break;
+ case tparquet::Type::DOUBLE:
+ ans.first =
DataTypeFactory::instance().create_data_type(TYPE_DOUBLE, nullable);
+ break;
+ case tparquet::Type::BYTE_ARRAY:
+ if (_enable_mapping_varbinary) {
+ // if physical_schema not set logicalType and converted_type,
+ // we treat BYTE_ARRAY as VARBINARY by default, so that we can
read all data directly.
+ ans.first =
DataTypeFactory::instance().create_data_type(TYPE_VARBINARY, nullable);
+ } else {
+ ans.first =
DataTypeFactory::instance().create_data_type(TYPE_STRING, nullable);
+ }
+ break;
+ case tparquet::Type::FIXED_LEN_BYTE_ARRAY:
+ ans.first =
DataTypeFactory::instance().create_data_type(TYPE_STRING, nullable);
+ break;
+ default:
+ throw Exception(Status::InternalError("Not supported parquet
logicalType{}",
+ physical_schema.type));
+ break;
+ }
+ }
+ return ans;
+}
+
+std::pair<DataTypePtr, bool> NativeFieldDescriptor::convert_to_doris_type(
+ tparquet::LogicalType logicalType, bool nullable) {
+ std::pair<DataTypePtr, bool> ans = {std::make_shared<DataTypeNothing>(),
false};
+ bool& is_type_compatibility = ans.second;
+ if (logicalType.__isset.STRING) {
+ ans.first = DataTypeFactory::instance().create_data_type(TYPE_STRING,
nullable);
+ } else if (logicalType.__isset.DECIMAL) {
+ ans.first =
DataTypeFactory::instance().create_data_type(TYPE_DECIMAL128I, nullable,
+
logicalType.DECIMAL.precision,
+
logicalType.DECIMAL.scale);
+ } else if (logicalType.__isset.DATE) {
+ ans.first = DataTypeFactory::instance().create_data_type(TYPE_DATEV2,
nullable);
+ } else if (logicalType.__isset.INTEGER) {
+ if (logicalType.INTEGER.isSigned) {
+ if (logicalType.INTEGER.bitWidth <= 8) {
+ ans.first =
DataTypeFactory::instance().create_data_type(TYPE_TINYINT, nullable);
+ } else if (logicalType.INTEGER.bitWidth <= 16) {
+ ans.first =
DataTypeFactory::instance().create_data_type(TYPE_SMALLINT, nullable);
+ } else if (logicalType.INTEGER.bitWidth <= 32) {
+ ans.first =
DataTypeFactory::instance().create_data_type(TYPE_INT, nullable);
+ } else {
+ ans.first =
DataTypeFactory::instance().create_data_type(TYPE_BIGINT, nullable);
+ }
+ } else {
+ is_type_compatibility = true;
+ if (logicalType.INTEGER.bitWidth <= 8) {
+ ans.first =
DataTypeFactory::instance().create_data_type(TYPE_SMALLINT, nullable);
+ } else if (logicalType.INTEGER.bitWidth <= 16) {
+ ans.first =
DataTypeFactory::instance().create_data_type(TYPE_INT, nullable);
+ } else if (logicalType.INTEGER.bitWidth <= 32) {
+ ans.first =
DataTypeFactory::instance().create_data_type(TYPE_BIGINT, nullable);
+ } else {
+ ans.first =
DataTypeFactory::instance().create_data_type(TYPE_LARGEINT, nullable);
+ }
+ }
+ } else if (logicalType.__isset.TIME) {
+ ans.first = DataTypeFactory::instance().create_data_type(TYPE_TIMEV2,
nullable);
+ } else if (logicalType.__isset.TIMESTAMP) {
+ if (_enable_mapping_timestamp_tz) {
+ if (logicalType.TIMESTAMP.isAdjustedToUTC) {
+ // treat TIMESTAMP with isAdjustedToUTC as TIMESTAMPTZ
+ ans.first = DataTypeFactory::instance().create_data_type(
+ TYPE_TIMESTAMPTZ, nullable, 0,
+ logicalType.TIMESTAMP.unit.__isset.MILLIS ? 3 : 6);
+ return ans;
+ }
+ }
+ ans.first = DataTypeFactory::instance().create_data_type(
+ TYPE_DATETIMEV2, nullable, 0,
logicalType.TIMESTAMP.unit.__isset.MILLIS ? 3 : 6);
+ } else if (logicalType.__isset.JSON) {
+ ans.first = DataTypeFactory::instance().create_data_type(TYPE_STRING,
nullable);
+ } else if (logicalType.__isset.UUID) {
+ if (_enable_mapping_varbinary) {
+ ans.first =
DataTypeFactory::instance().create_data_type(TYPE_VARBINARY, nullable, -1,
+ -1, 16);
+ } else {
+ ans.first =
DataTypeFactory::instance().create_data_type(TYPE_STRING, nullable);
+ }
+ } else if (logicalType.__isset.FLOAT16) {
+ ans.first = DataTypeFactory::instance().create_data_type(TYPE_FLOAT,
nullable);
+ } else {
Review Comment:
[P1] Preserve STRING inference for ENUM and BSON leaves This mapper omits
logical ENUM/BSON, and the converted-type overload omits their legacy
annotations too. Both cases therefore throw into `get_doris_type()`'s
BYTE_ARRAY fallback; V2 enables varbinary mapping, so valid ENUM/BSON columns
are now exposed as VARBINARY. The established `parquet_type.cpp` resolver and
this native decoder both classify these annotations as STRING. Handle ENUM/BSON
alongside STRING/JSON in both overloads and cover valid annotated leaves, not
only the invalid ENUM-group case.
##########
be/src/format_v2/parquet/native_schema_desc.cpp:
##########
@@ -0,0 +1,779 @@
+// 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() || !is_group_node(schemas[0])) {
+ return Status::InvalidArgument("Wrong parquet root schema element");
+ }
+
+ struct PendingGroup {
+ size_t remaining_children;
+ size_t depth;
+ };
+ const auto root_children = num_children_node(schemas[0]);
+ if (root_children < 0 || static_cast<size_t>(root_children) >
schemas.size() - 1) {
+ return Status::InvalidArgument("Invalid parquet root child count {}",
root_children);
+ }
+ std::vector<PendingGroup> pending {{static_cast<size_t>(root_children),
0}};
+ for (size_t pos = 1; pos < schemas.size(); ++pos) {
+ while (!pending.empty() && pending.back().remaining_children == 0) {
+ pending.pop_back();
+ }
+ if (pending.empty()) {
+ return Status::InvalidArgument("Schema element {} is not reachable
from the root", pos);
+ }
+
+ const size_t depth = pending.back().depth + 1;
+ --pending.back().remaining_children;
+ const int children = num_children_node(schemas[pos]);
Review Comment:
[P1] Validate SchemaElement kind and repetition before parsing This pass
bounds `num_children`, but it never enforces the Parquet contract that groups
set `num_children` and no `type`, primitives set `type` and no `num_children`,
and every non-root node sets `repetition_type`. With neither kind field, the
node is treated as a leaf and the generated default type (BOOLEAN) is consumed;
with no repetition bit it is treated as REQUIRED. For an optional Page V1 leaf
that can leave definition-level bytes at the value cursor instead of returning
corruption. Validate these isset invariants here before building the tree and
add missing/dual-kind plus missing-repetition footer tests.
##########
be/src/format_v2/parquet/reader/native/page_reader.cpp:
##########
@@ -0,0 +1,270 @@
+// 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 ||
+ static_cast<uint64_t>(_cur_page_header.compressed_page_size) >
+ _end_offset - _offset - header_size)) {
+ // Sizes are untrusted signed Thrift fields and must fit the column
chunk before arithmetic.
+ return Status::Corruption("Parquet page payload exceeds its column
chunk");
+ }
+ if (_cur_page_header.__isset.data_page_header_v2) {
Review Comment:
[P1] Validate the page-type discriminant before choosing a layout
`PageHeader.type` is the contract that identifies which single page-specific
header is set, but this validation and `is_header_v2()` choose V2 solely from
`__isset.data_page_header_v2`. A DATA_PAGE_V2 carrying only a V1 member is
decoded as V1, a DATA_PAGE carrying a V2 member is decoded as V2, and
absent/dual members are not rejected before level, compression, and payload
cursors are selected. Require the matching member (and reject competing
members) for each page type, with malformed swapped/absent/dual-header tests.
##########
be/src/format_v2/parquet/native_schema_desc.cpp:
##########
@@ -0,0 +1,779 @@
+// 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() || !is_group_node(schemas[0])) {
+ return Status::InvalidArgument("Wrong parquet root schema element");
+ }
+
+ struct PendingGroup {
+ size_t remaining_children;
+ size_t depth;
+ };
+ const auto root_children = num_children_node(schemas[0]);
+ if (root_children < 0 || static_cast<size_t>(root_children) >
schemas.size() - 1) {
+ return Status::InvalidArgument("Invalid parquet root child count {}",
root_children);
+ }
+ std::vector<PendingGroup> pending {{static_cast<size_t>(root_children),
0}};
+ for (size_t pos = 1; pos < schemas.size(); ++pos) {
+ while (!pending.empty() && pending.back().remaining_children == 0) {
+ pending.pop_back();
+ }
+ if (pending.empty()) {
+ return Status::InvalidArgument("Schema element {} is not reachable
from the root", pos);
+ }
+
+ const size_t depth = pending.back().depth + 1;
+ --pending.back().remaining_children;
+ const int children = num_children_node(schemas[pos]);
+ if (children < 0 || static_cast<size_t>(children) > schemas.size() -
pos - 1) {
+ return Status::InvalidArgument("Invalid child count {} at schema
element {}", children,
+ pos);
+ }
+ if (children > 0) {
+ // Bound the tree before any resize or recursion so an untrusted
footer cannot
+ // turn a tiny schema into an unbounded allocation or parser stack.
+ if (depth > MAX_NATIVE_SCHEMA_DEPTH) {
+ return Status::InvalidArgument("Parquet schema depth {}
exceeds limit {}", depth,
+ MAX_NATIVE_SCHEMA_DEPTH);
+ }
+ pending.push_back({static_cast<size_t>(children), depth});
+ }
+ }
+ while (!pending.empty() && pending.back().remaining_children == 0) {
+ pending.pop_back();
+ }
+ if (!pending.empty()) {
+ return Status::InvalidArgument("Parquet schema ended before all
children were parsed");
+ }
+ return Status::OK();
+}
+
+/**
+ * `repeated_parent_def_level` is the definition level of the first ancestor
node whose repetition_type equals REPEATED.
+ * Empty array/map values are not stored in doris columns, so have to use
`repeated_parent_def_level` to skip the
+ * empty or null values in ancestor node.
+ *
+ * For instance, considering an array of strings with 3 rows like the
following:
+ * null, [], [a, b, c]
+ * We can store four elements in data column: null, a, b, c
+ * and the offsets column is: 1, 1, 4
+ * and the null map is: 1, 0, 0
+ * For the i-th row in array column: range from `offsets[i - 1]` until
`offsets[i]` represents the elements in this row,
+ * so we can't store empty array/map values in doris data column.
+ * As a comparison, spark does not require `repeated_parent_def_level`,
+ * because the spark column stores empty array/map values , and use anther
length column to indicate empty values.
+ * Please reference:
https://github.com/apache/spark/blob/master/sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/ParquetColumnVector.java
+ *
+ * Furthermore, we can also avoid store null array/map values in doris data
column.
+ * The same three rows as above, We can only store three elements in data
column: a, b, c
+ * and the offsets column is: 0, 0, 3
+ * and the null map is: 1, 0, 0
+ *
+ * Inherit the repetition and definition level from parent node, if the parent
node is repeated,
+ * we should set repeated_parent_def_level = definition_level, otherwise as
repeated_parent_def_level.
+ * @param parent parent node
+ * @param repeated_parent_def_level the first ancestor node whose
repetition_type equals REPEATED
+ */
+static void set_child_node_level(NativeFieldSchema* parent, int16_t
repeated_parent_def_level) {
+ for (auto& child : parent->children) {
+ child.repetition_level = parent->repetition_level;
+ child.definition_level = parent->definition_level;
+ child.repeated_parent_def_level = repeated_parent_def_level;
+ }
+}
+
+static bool is_struct_list_node(const tparquet::SchemaElement& schema) {
+ const std::string& name = schema.name;
+ static const Slice array_slice("array", 5);
+ static const Slice tuple_slice("_tuple", 6);
+ Slice slice(name);
+ return slice == array_slice || slice.ends_with(tuple_slice);
+}
+
+std::string NativeFieldSchema::debug_string() const {
+ std::stringstream ss;
+ ss << "NativeFieldSchema(name=" << name << ", R=" << repetition_level
+ << ", D=" << definition_level;
+ if (children.size() > 0) {
+ ss << ", type=" << data_type->get_name() << ", children=[";
+ for (int i = 0; i < children.size(); ++i) {
+ if (i != 0) {
+ ss << ", ";
+ }
+ ss << children[i].debug_string();
+ }
+ ss << "]";
+ } else {
+ ss << ", physical_type=" << physical_type;
+ ss << " , doris_type=" << data_type->get_name();
+ }
+ ss << ")";
+ return ss.str();
+}
+
+Status NativeFieldDescriptor::parse_from_thrift(
+ const std::vector<tparquet::SchemaElement>& t_schemas) {
+ _fields.clear();
+ _physical_fields.clear();
+ _name_to_field.clear();
+ RETURN_IF_ERROR(validate_native_schema_structure(t_schemas));
+ const auto& root_schema = t_schemas[0];
+ _fields.resize(root_schema.num_children);
+ _next_schema_pos = 1;
+
+ for (int i = 0; i < root_schema.num_children; ++i) {
+ RETURN_IF_ERROR(parse_node_field(t_schemas, _next_schema_pos,
&_fields[i]));
+ if (_name_to_field.find(_fields[i].name) != _name_to_field.end()) {
+ return Status::InvalidArgument("Duplicated field name: {}",
_fields[i].name);
+ }
+ _name_to_field.emplace(_fields[i].name, &_fields[i]);
+ }
+
+ if (_next_schema_pos != t_schemas.size()) {
+ return Status::InvalidArgument("Remaining {} unparsed schema elements",
+ t_schemas.size() - _next_schema_pos);
+ }
+
+ return Status::OK();
+}
+
+Status NativeFieldDescriptor::parse_node_field(
+ const std::vector<tparquet::SchemaElement>& t_schemas, size_t curr_pos,
+ NativeFieldSchema* node_field) {
+ if (curr_pos >= t_schemas.size()) {
+ return Status::InvalidArgument("Out-of-bounds index of schema
elements");
+ }
+ auto& t_schema = t_schemas[curr_pos];
+ if (is_group_node(t_schema)) {
+ // nested structure or nullable list
+ return parse_group_field(t_schemas, curr_pos, node_field);
+ }
+ if (is_repeated_node(t_schema)) {
+ // repeated <primitive-type> <name> (LIST)
+ // produce required list<element>
+ node_field->repetition_level++;
+ node_field->definition_level++;
+ node_field->children.resize(1);
+ set_child_node_level(node_field, node_field->definition_level);
+ auto child = &node_field->children[0];
+ parse_physical_field(t_schema, false, child);
+
+ node_field->name = t_schema.name;
+ node_field->lower_case_name = to_lower(t_schema.name);
+ node_field->data_type =
std::make_shared<DataTypeArray>(make_nullable(child->data_type));
+ _next_schema_pos = curr_pos + 1;
+ node_field->field_id = t_schema.__isset.field_id ? t_schema.field_id :
-1;
+ } else {
+ bool is_optional = is_optional_node(t_schema);
+ if (is_optional) {
+ node_field->definition_level++;
+ }
+ parse_physical_field(t_schema, is_optional, node_field);
+ _next_schema_pos = curr_pos + 1;
+ }
+ return Status::OK();
+}
+
+void NativeFieldDescriptor::parse_physical_field(const
tparquet::SchemaElement& physical_schema,
+ bool is_nullable,
+ NativeFieldSchema*
physical_field) {
+ physical_field->name = physical_schema.name;
+ physical_field->lower_case_name = to_lower(physical_field->name);
+ physical_field->parquet_schema = physical_schema;
+ physical_field->physical_type = physical_schema.type;
+ physical_field->column_id = NATIVE_UNASSIGNED_COLUMN_ID; // Initialize
column_id
+ _physical_fields.push_back(physical_field);
+ physical_field->physical_column_index =
cast_set<int>(_physical_fields.size() - 1);
+ auto type = get_doris_type(physical_schema, is_nullable);
+ physical_field->data_type = type.first;
+ physical_field->is_type_compatibility = type.second;
+ physical_field->field_id = physical_schema.__isset.field_id ?
physical_schema.field_id : -1;
+}
+
+std::pair<DataTypePtr, bool> NativeFieldDescriptor::get_doris_type(
+ const tparquet::SchemaElement& physical_schema, bool nullable) {
+ std::pair<DataTypePtr, bool> ans = {std::make_shared<DataTypeNothing>(),
false};
+ try {
+ if (physical_schema.__isset.logicalType) {
+ ans = convert_to_doris_type(physical_schema.logicalType, nullable);
+ } else if (physical_schema.__isset.converted_type) {
+ ans = convert_to_doris_type(physical_schema, nullable);
+ }
+ } catch (...) {
+ // now the Not supported exception are ignored
+ // so those byte_array maybe be treated as varbinary(now) :
string(before)
+ }
+ if (ans.first->get_primitive_type() == PrimitiveType::INVALID_TYPE) {
+ switch (physical_schema.type) {
+ case tparquet::Type::BOOLEAN:
+ ans.first =
DataTypeFactory::instance().create_data_type(TYPE_BOOLEAN, nullable);
+ break;
+ case tparquet::Type::INT32:
+ ans.first = DataTypeFactory::instance().create_data_type(TYPE_INT,
nullable);
+ break;
+ case tparquet::Type::INT64:
+ ans.first =
DataTypeFactory::instance().create_data_type(TYPE_BIGINT, nullable);
+ break;
+ case tparquet::Type::INT96:
+ if (_enable_mapping_timestamp_tz) {
+ // treat INT96 as TIMESTAMPTZ
+ ans.first =
DataTypeFactory::instance().create_data_type(TYPE_TIMESTAMPTZ, nullable,
+ 0, 6);
+ } else {
+ // in most cases, it's a nano timestamp
+ ans.first =
DataTypeFactory::instance().create_data_type(TYPE_DATETIMEV2, nullable,
+ 0, 6);
+ }
+ break;
+ case tparquet::Type::FLOAT:
+ ans.first =
DataTypeFactory::instance().create_data_type(TYPE_FLOAT, nullable);
+ break;
+ case tparquet::Type::DOUBLE:
+ ans.first =
DataTypeFactory::instance().create_data_type(TYPE_DOUBLE, nullable);
+ break;
+ case tparquet::Type::BYTE_ARRAY:
+ if (_enable_mapping_varbinary) {
+ // if physical_schema not set logicalType and converted_type,
+ // we treat BYTE_ARRAY as VARBINARY by default, so that we can
read all data directly.
+ ans.first =
DataTypeFactory::instance().create_data_type(TYPE_VARBINARY, nullable);
+ } else {
+ ans.first =
DataTypeFactory::instance().create_data_type(TYPE_STRING, nullable);
+ }
+ break;
+ case tparquet::Type::FIXED_LEN_BYTE_ARRAY:
+ ans.first =
DataTypeFactory::instance().create_data_type(TYPE_STRING, nullable);
+ break;
+ default:
+ throw Exception(Status::InternalError("Not supported parquet
logicalType{}",
+ physical_schema.type));
+ break;
+ }
+ }
+ return ans;
+}
+
+std::pair<DataTypePtr, bool> NativeFieldDescriptor::convert_to_doris_type(
+ tparquet::LogicalType logicalType, bool nullable) {
+ std::pair<DataTypePtr, bool> ans = {std::make_shared<DataTypeNothing>(),
false};
+ bool& is_type_compatibility = ans.second;
+ if (logicalType.__isset.STRING) {
+ ans.first = DataTypeFactory::instance().create_data_type(TYPE_STRING,
nullable);
+ } else if (logicalType.__isset.DECIMAL) {
+ ans.first =
DataTypeFactory::instance().create_data_type(TYPE_DECIMAL128I, nullable,
+
logicalType.DECIMAL.precision,
+
logicalType.DECIMAL.scale);
+ } else if (logicalType.__isset.DATE) {
+ ans.first = DataTypeFactory::instance().create_data_type(TYPE_DATEV2,
nullable);
+ } else if (logicalType.__isset.INTEGER) {
+ if (logicalType.INTEGER.isSigned) {
+ if (logicalType.INTEGER.bitWidth <= 8) {
+ ans.first =
DataTypeFactory::instance().create_data_type(TYPE_TINYINT, nullable);
+ } else if (logicalType.INTEGER.bitWidth <= 16) {
+ ans.first =
DataTypeFactory::instance().create_data_type(TYPE_SMALLINT, nullable);
+ } else if (logicalType.INTEGER.bitWidth <= 32) {
+ ans.first =
DataTypeFactory::instance().create_data_type(TYPE_INT, nullable);
+ } else {
+ ans.first =
DataTypeFactory::instance().create_data_type(TYPE_BIGINT, nullable);
+ }
+ } else {
+ is_type_compatibility = true;
+ if (logicalType.INTEGER.bitWidth <= 8) {
+ ans.first =
DataTypeFactory::instance().create_data_type(TYPE_SMALLINT, nullable);
+ } else if (logicalType.INTEGER.bitWidth <= 16) {
+ ans.first =
DataTypeFactory::instance().create_data_type(TYPE_INT, nullable);
+ } else if (logicalType.INTEGER.bitWidth <= 32) {
+ ans.first =
DataTypeFactory::instance().create_data_type(TYPE_BIGINT, nullable);
+ } else {
+ ans.first =
DataTypeFactory::instance().create_data_type(TYPE_LARGEINT, nullable);
+ }
+ }
+ } else if (logicalType.__isset.TIME) {
+ ans.first = DataTypeFactory::instance().create_data_type(TYPE_TIMEV2,
nullable);
Review Comment:
[P1] Derive TIMEV2 scale from the Parquet time unit The factory default here
is scale 0, while valid logical TIME(MILLIS/MICROS) carries fractional
precision and the native decoder preserves that unit and materializes
microseconds. The established `parquet_type.cpp` resolver reports TIMEV2(3) and
TIMEV2(6); returning TIMEV2(0) from the new inferred schema suppresses those
fractional digits at serialization. Map the unit to the same scale and add
inferred-schema/value coverage for both units.
##########
be/src/format_v2/parquet/reader/native_column_reader.cpp:
##########
@@ -0,0 +1,678 @@
+// 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_column_reader.h"
+
+#include <algorithm>
+#include <cstddef>
+#include <limits>
+#include <ranges>
+#include <string>
+#include <utility>
+
+#include "common/cast_set.h"
+#include "common/config.h"
+#include "core/assert_cast.h"
+#include "core/column/column_nullable.h"
+#include "core/column/column_string.h"
+#include "core/column/column_vector.h"
+#include "core/data_type/data_type_array.h"
+#include "core/data_type/data_type_map.h"
+#include "core/data_type/data_type_nullable.h"
+#include "core/data_type/data_type_number.h"
+#include "core/data_type/data_type_struct.h"
+#include "format_v2/column_data.h"
+#include "format_v2/parquet/parquet_column_schema.h"
+#include "format_v2/parquet/parquet_file_context.h"
+#include "runtime/runtime_state.h"
+
+namespace doris::format::parquet {
+namespace {
+
+constexpr size_t MAX_RETAINED_BATCH_SCRATCH_BYTES = 4UL << 20;
+
+DataTypePtr projected_type(const ParquetColumnSchema& schema,
+ const format::LocalColumnIndex* projection) {
+ if (!format::is_partial_projection(projection)) {
+ return schema.type;
+ }
+ switch (schema.kind) {
+ case ParquetColumnSchemaKind::PRIMITIVE:
+ return schema.type;
+ case ParquetColumnSchemaKind::STRUCT: {
+ DataTypes child_types;
+ Strings child_names;
+ child_types.reserve(projection->children.size());
+ child_names.reserve(projection->children.size());
+ for (const auto& child_projection : projection->children) {
+ const auto child_it = std::ranges::find_if(schema.children,
[&](const auto& child) {
+ return child->local_id == child_projection.local_id();
+ });
+ DORIS_CHECK(child_it != schema.children.end());
+ child_types.push_back(make_nullable(projected_type(**child_it,
&child_projection)));
+ child_names.push_back((*child_it)->name);
+ }
+ DataTypePtr type = std::make_shared<DataTypeStruct>(child_types,
child_names);
+ return schema.type->is_nullable() ? make_nullable(type) : type;
+ }
+ case ParquetColumnSchemaKind::LIST: {
+ DORIS_CHECK(schema.children.size() == 1);
+ const auto* child_projection =
+ format::find_child_projection(projection,
schema.children[0]->local_id);
+ DORIS_CHECK(child_projection != nullptr);
+ DataTypePtr type = std::make_shared<DataTypeArray>(
+ projected_type(*schema.children[0], child_projection));
+ return schema.type->is_nullable() ? make_nullable(type) : type;
+ }
+ case ParquetColumnSchemaKind::MAP: {
+ DORIS_CHECK(schema.children.size() == 2);
+ const auto* value_projection =
+ format::find_child_projection(projection,
schema.children[1]->local_id);
+ DORIS_CHECK(value_projection != nullptr);
+ DataTypePtr type = std::make_shared<DataTypeMap>(
+ make_nullable(schema.children[0]->type),
+ make_nullable(projected_type(*schema.children[1],
value_projection)));
+ return schema.type->is_nullable() ? make_nullable(type) : type;
+ }
+ }
+ DORIS_CHECK(false);
+ return nullptr;
+}
+
+const NativeFieldSchema* find_child_field(const NativeFieldSchema& parent,
+ const ParquetColumnSchema& child) {
+ auto field_it = std::ranges::find_if(parent.children, [&](const
NativeFieldSchema& field) {
+ return (child.parquet_field_id >= 0 && field.field_id ==
child.parquet_field_id) ||
+ field.name == child.name;
+ });
+ return field_it == parent.children.end() ? nullptr : &*field_it;
+}
+
+void collect_physical_subtree_ids(const NativeFieldSchema& field,
std::set<uint64_t>* ids) {
+ DORIS_CHECK(ids != nullptr);
+ ids->insert(field.get_column_id());
+ for (const auto& child : field.children) {
+ collect_physical_subtree_ids(child, ids);
+ }
+}
+
+void collect_projected_ids(const ParquetColumnSchema& schema,
+ const format::LocalColumnIndex* projection,
+ const NativeFieldSchema& native_field,
std::set<uint64_t>* ids) {
+ DORIS_CHECK(ids != nullptr);
+ if (!format::is_partial_projection(projection)) {
+ return;
+ }
+ for (const auto& child_projection : projection->children) {
+ const auto schema_it = std::ranges::find_if(schema.children, [&](const
auto& child) {
+ return child->local_id == child_projection.local_id();
+ });
+ DORIS_CHECK(schema_it != schema.children.end());
+ const NativeFieldSchema* child_field = find_child_field(native_field,
**schema_it);
+ DORIS_CHECK(child_field != nullptr);
+ if (format::is_full_projection(&child_projection)) {
+ // A full child path is a request for its complete physical
subtree. Keeping only the
+ // child group id makes its grandchildren SkipReadingReaders and
silently defaults
+ // STRUCT fields (or breaks ARRAY/MAP shape invariants).
+ collect_physical_subtree_ids(*child_field, ids);
+ } else {
+ ids->insert(child_field->get_column_id());
+ collect_projected_ids(**schema_it, &child_projection,
*child_field, ids);
+ }
+ }
+ if (schema.kind == ParquetColumnSchemaKind::MAP) {
+ DORIS_CHECK(!native_field.children.empty());
+ // MAP entry existence and offsets are owned by the key stream even
for value-only
+ // projections. Keep the key reader live so the native complex reader
can validate
+ // key/value entry alignment while constructing offsets.
+ ids->insert(native_field.children[0].get_column_id());
+ }
+}
+
+Status append_non_null_dictionary_values(MutableColumnPtr& target,
MutableColumnPtr values) {
+ DORIS_CHECK(target);
+ DORIS_CHECK(values);
+ const size_t value_count = values->size();
+ if (auto* nullable = check_and_get_column<ColumnNullable>(*target);
nullable != nullptr) {
+ nullable->get_nested_column().insert_range_from(*values, 0,
value_count);
+ auto& null_map = nullable->get_null_map_data();
+ null_map.resize_fill(null_map.size() + value_count, 0);
+ return Status::OK();
+ }
+ target->insert_range_from(*values, 0, value_count);
+ return Status::OK();
+}
+
+} // namespace
+
+NativeColumnReader::NativeColumnReader(const ParquetColumnSchema& schema,
+ DataTypePtr projected_type,
+ ParquetColumnReaderProfile profile)
+ : ParquetColumnReader(schema, std::move(projected_type), profile),
+ _nested(schema.kind != ParquetColumnSchemaKind::PRIMITIVE) {}
+
+NativeColumnReader::~NativeColumnReader() {
+ (void)sync_native_profile();
+}
+
+Status NativeColumnReader::create(
+ const ParquetColumnSchema& column_schema, const
format::LocalColumnIndex* projection,
+ io::FileReaderSPtr file, const NativeParquetMetadata* metadata, int
row_group_id,
+ const std::vector<RowRange>& selected_ranges,
+ const std::unordered_map<int, tparquet::OffsetIndex>& offset_indexes,
+ const cctz::time_zone* timezone, io::IOContext* io_ctx, RuntimeState*
runtime_state,
+ bool enable_page_cache, const std::string& page_cache_file_key,
+ bool enable_dictionary_filter, ParquetColumnReaderProfile profile,
+ std::unique_ptr<ParquetColumnReader>* reader) {
+ if (reader == nullptr) {
+ return Status::InvalidArgument("Native parquet reader result is null");
+ }
+ if (file == nullptr || metadata == nullptr) {
+ return Status::InvalidArgument("Native parquet file context is not
initialized");
+ }
+ if (row_group_id < 0 ||
+ row_group_id >=
static_cast<int>(metadata->to_thrift().row_groups.size())) {
+ return Status::InvalidArgument("Invalid native parquet row group {}",
row_group_id);
+ }
+ const auto& native_schema = metadata->schema();
+ if (column_schema.local_id < 0 || column_schema.local_id >=
native_schema.size()) {
+ return Status::InvalidArgument("Invalid native parquet top-level
column id {} for {}",
+ column_schema.local_id,
column_schema.name);
+ }
+ auto* field =
const_cast<NativeFieldSchema*>(native_schema.get_column(column_schema.local_id));
+ DORIS_CHECK(field != nullptr);
+ if (field->name != column_schema.name &&
+ !(field->field_id >= 0 && field->field_id ==
column_schema.parquet_field_id)) {
+ return Status::Corruption(
+ "Native/metadata parquet schema mismatch at column {}:
native={}, arrow={}",
+ column_schema.local_id, field->name, column_schema.name);
+ }
+
+ auto type = projected_type(column_schema, projection);
+ std::shared_ptr<NativeSchemaNode> schema_node;
+ RETURN_IF_ERROR(build_native_schema_node(type, column_schema,
&schema_node));
+ std::set<uint64_t> projected_ids;
+ collect_projected_ids(column_schema, projection, *field, &projected_ids);
+
+ auto native_reader = std::unique_ptr<NativeColumnReader>(
+ new NativeColumnReader(column_schema, std::move(type), profile));
+ RETURN_IF_ERROR(native_reader->init(
+ std::move(file), metadata, row_group_id, field,
std::move(schema_node),
+ std::move(projected_ids), selected_ranges, offset_indexes,
timezone, io_ctx,
+ runtime_state, enable_page_cache, page_cache_file_key,
enable_dictionary_filter));
+ *reader = std::move(native_reader);
+ return Status::OK();
+}
+
+Status NativeColumnReader::init(
+ io::FileReaderSPtr file, const NativeParquetMetadata* metadata, int
row_group_id,
+ NativeFieldSchema* field, std::shared_ptr<NativeSchemaNode>
schema_node,
+ std::set<uint64_t> projected_column_ids, const std::vector<RowRange>&
selected_ranges,
+ const std::unordered_map<int, tparquet::OffsetIndex>& offset_indexes,
+ const cctz::time_zone* timezone, io::IOContext* io_ctx, RuntimeState*
runtime_state,
+ bool enable_page_cache, const std::string& page_cache_file_key,
+ bool enable_dictionary_filter) {
+ DORIS_CHECK(file != nullptr);
+ DORIS_CHECK(metadata != nullptr);
+ DORIS_CHECK(field != nullptr);
+ DORIS_CHECK(schema_node != nullptr);
+ const auto& row_group = metadata->to_thrift().row_groups[row_group_id];
+ DORIS_CHECK(row_group.num_rows > 0);
+ _row_group_rows = row_group.num_rows;
+ _selected_ranges = selected_ranges;
+ DORIS_CHECK(!_selected_ranges.empty());
+ for (const auto& range : _selected_ranges) {
+ DORIS_CHECK(range.start >= 0);
+ DORIS_CHECK(range.length > 0);
+ DORIS_CHECK(range.start + range.length <= _row_group_rows);
+ _row_ranges.add(::doris::RowRange(range.start, range.start +
range.length));
+ }
+ // Offset indexes are immutable row-group metadata owned by
ParquetScanScheduler. Sharing them
+ // avoids retaining the full N-column page-location map once per projected
reader.
+ _offset_indexes = &offset_indexes;
+ _schema_node = std::move(schema_node);
+ _projected_column_ids = std::move(projected_column_ids);
+ _dictionary_filter_enabled = enable_dictionary_filter;
+
+ const size_t max_group_buffer = config::parquet_rowgroup_max_buffer_mb <<
20;
+ const size_t max_column_buffer = config::parquet_column_max_buffer_mb <<
20;
+ const size_t max_buffer_size = std::min(max_group_buffer,
max_column_buffer);
+ RuntimeState* native_runtime_state = runtime_state;
+ const bool runtime_page_cache_enabled =
+ runtime_state == nullptr ||
+ runtime_state->query_options().enable_parquet_file_page_cache;
+ if (runtime_page_cache_enabled != enable_page_cache) {
+ TQueryOptions query_options =
+ runtime_state == nullptr ? TQueryOptions() :
runtime_state->query_options();
+ query_options.__set_enable_parquet_file_page_cache(enable_page_cache);
+ _page_cache_runtime_state = RuntimeState::create_unique(query_options,
TQueryGlobals());
+ native_runtime_state = _page_cache_runtime_state.get();
+ }
+ const auto& thrift_metadata = metadata->to_thrift();
+ const auto compat = native::parquet_reader_compat(
+ thrift_metadata.__isset.created_by ? thrift_metadata.created_by :
"");
+ RETURN_IF_ERROR(native::ColumnReader::create(
+ std::move(file), field, row_group, _row_ranges, timezone, io_ctx,
_native_reader,
+ max_buffer_size, *_offset_indexes, native_runtime_state, false,
_projected_column_ids,
+ _filter_column_ids, page_cache_file_key, compat,
+ runtime_state != nullptr && runtime_state->enable_strict_mode()));
+ DORIS_CHECK(_native_reader != nullptr);
+ _skip_column = _type->create_column();
+ return Status::OK();
+}
+
+Status NativeColumnReader::read_with_filter(int64_t rows, const uint8_t*
filter_data,
+ bool filter_all, MutableColumnPtr&
column,
+ const DataTypePtr& output_type,
bool dictionary_ids,
+ int64_t* rows_read) {
+ DORIS_CHECK(rows >= 0);
+ DORIS_CHECK(column);
+ DORIS_CHECK(output_type != nullptr);
+ DORIS_CHECK(rows_read != nullptr);
+ *rows_read = 0;
+ if (rows == 0) {
+ return Status::OK();
+ }
+
+ native::FilterMap filter;
+ RETURN_IF_ERROR(filter.init(filter_data, static_cast<size_t>(rows),
filter_all));
+ _native_reader->reset_filter_map_index();
+ ColumnPtr native_column(std::move(column));
+ bool eof = false;
+ int64_t native_calls = 0;
+ int64_t consecutive_empty_calls = 0;
+ while (*rows_read < rows && !eof) {
+ ++native_calls;
+ size_t loop_rows = 0;
+ RETURN_IF_ERROR(_native_reader->read_column_data(
+ native_column, output_type, _schema_node, filter,
+ static_cast<size_t>(rows - *rows_read), &loop_rows, &eof,
dictionary_ids));
+ if (loop_rows == 0 && !eof) {
+ // A selected RowRanges plan may reject the current data page
completely. V1 advances
+ // the page cursor and deliberately returns zero rows so the
caller can request the
+ // next page. Bound consecutive empty transitions by the Row Group
row count to retain
+ // a deterministic corruption exit if a decoder ever stops
advancing.
+ if (++consecutive_empty_calls > _row_group_rows + 1) {
+ column = IColumn::mutate(std::move(native_column));
+ return Status::Corruption("Native parquet reader made no
progress for column {}",
+ _name);
+ }
+ continue;
+ }
+ consecutive_empty_calls = 0;
+ *rows_read += static_cast<int64_t>(loop_rows);
+ }
+ column = IColumn::mutate(std::move(native_column));
+ if (_profile.native_read_calls != nullptr) {
+ COUNTER_UPDATE(_profile.native_read_calls, native_calls);
+ }
+ if (_nested && _profile.nested_batches != nullptr) {
+ COUNTER_UPDATE(_profile.nested_batches, 1);
+ }
+ // Retained-capacity inspection walks the native reader tree. Check it
periodically instead of
+ // on every small batch; row-group destruction is still the hard lifetime
bound for scratch.
+ constexpr size_t SCRATCH_CHECK_BATCH_INTERVAL = 16;
+ if (++_batches_since_scratch_check >= SCRATCH_CHECK_BATCH_INTERVAL) {
+
_native_reader->release_batch_scratch(MAX_RETAINED_BATCH_SCRATCH_BYTES);
+ _batches_since_scratch_check = 0;
+ }
+ if (*rows_read != rows) {
+ return Status::Corruption("Native parquet reader returned {} rows,
expected {} for {}",
+ *rows_read, rows, _name);
+ }
+ return Status::OK();
+}
+
+Status NativeColumnReader::validate_selected_span(int64_t rows) {
+ DORIS_CHECK(rows >= 0);
+ while (_selected_range_idx < _selected_ranges.size()) {
+ const auto& range = _selected_ranges[_selected_range_idx];
+ const int64_t range_end = range.start + range.length;
+ if (_logical_row_position < range_end) {
+ break;
+ }
+ ++_selected_range_idx;
+ }
+ if (_selected_range_idx >= _selected_ranges.size()) {
+ return Status::Corruption("Native parquet read past selected ranges
for column {}", _name);
+ }
+ const auto& range = _selected_ranges[_selected_range_idx];
+ if (_logical_row_position < range.start ||
+ rows > range.start + range.length - _logical_row_position) {
+ return Status::Corruption(
+ "Native parquet read [{}, {}) crosses selected range [{}, {})
for column {}",
+ _logical_row_position, _logical_row_position + rows,
range.start,
+ range.start + range.length, _name);
+ }
+ return Status::OK();
+}
+
+void NativeColumnReader::advance_selected_span(int64_t rows) {
+ _logical_row_position += rows;
+ while (_selected_range_idx < _selected_ranges.size() &&
+ _logical_row_position >=
_selected_ranges[_selected_range_idx].start +
+
_selected_ranges[_selected_range_idx].length) {
+ ++_selected_range_idx;
+ }
+}
+
+Status NativeColumnReader::read(int64_t rows, MutableColumnPtr& column,
int64_t* rows_read) {
+ RETURN_IF_ERROR(validate_selected_span(rows));
+ RETURN_IF_ERROR(read_with_filter(rows, nullptr, false, column, _type,
false, rows_read));
+ advance_selected_span(*rows_read);
+ update_reader_read_rows(*rows_read);
+ return Status::OK();
+}
+
+Status NativeColumnReader::skip(int64_t rows) {
+ if (rows <= 0) {
+ return Status::OK();
+ }
+ DORIS_CHECK(_logical_row_position <= _row_group_rows - rows);
+ int64_t remaining = rows;
+ int64_t native_skipped_rows = 0;
+ while (remaining > 0) {
+ while (_selected_range_idx < _selected_ranges.size() &&
+ _logical_row_position >=
_selected_ranges[_selected_range_idx].start +
+
_selected_ranges[_selected_range_idx].length) {
+ ++_selected_range_idx;
+ }
+ if (_selected_range_idx >= _selected_ranges.size()) {
+ _logical_row_position += remaining;
+ break;
+ }
+ const auto& range = _selected_ranges[_selected_range_idx];
+ if (_logical_row_position < range.start) {
+ const int64_t gap = std::min(remaining, range.start -
_logical_row_position);
+ _logical_row_position += gap;
+ remaining -= gap;
+ continue;
+ }
+ const int64_t selected_rows = detail::bounded_native_lazy_skip_rows(
+ std::min(remaining, range.start + range.length -
_logical_row_position));
+ _skip_column->clear();
+ // Pending skips can span many filtered batches and are replayed for
every lazy column.
+ // Chunking here bounds each dense bitmap while preserving one logical
scheduler skip.
+ _filter_scratch.assign(static_cast<size_t>(selected_rows), 0);
+ int64_t rows_read = 0;
+ RETURN_IF_ERROR(read_with_filter(selected_rows,
_filter_scratch.data(), true, _skip_column,
+ _type, false, &rows_read));
+ DORIS_CHECK(_skip_column->empty());
+ DORIS_CHECK(rows_read == selected_rows);
+ _logical_row_position += rows_read;
+ native_skipped_rows += rows_read;
+ remaining -= rows_read;
+ }
+ update_reader_skip_rows(native_skipped_rows);
+ return Status::OK();
+}
+
+Status NativeColumnReader::select(const SelectionVector& selection, uint16_t
selected_rows,
+ int64_t batch_rows, MutableColumnPtr&
column) {
+ RETURN_IF_ERROR(validate_selected_span(batch_rows));
+ const uint8_t* filter_data = nullptr;
+ RETURN_IF_ERROR(selection.materialize_filter(selected_rows, batch_rows,
&filter_data));
+ const size_t old_size = column->size();
+ int64_t rows_read = 0;
+ RETURN_IF_ERROR(read_with_filter(batch_rows, filter_data, selected_rows ==
0, column, _type,
+ false, &rows_read));
+ advance_selected_span(rows_read);
+ if (column->size() != old_size + selected_rows) {
+ return Status::Corruption(
+ "Native parquet selection appended {} rows, expected {} for
column {}",
+ column->size() - old_size, selected_rows, _name);
+ }
+ if (_profile.reader_select_rows != nullptr) {
+ COUNTER_UPDATE(_profile.reader_select_rows, selected_rows);
+ }
+ update_reader_read_rows(selected_rows);
+ update_reader_skip_rows(batch_rows - selected_rows);
+ return Status::OK();
+}
+
+Status NativeColumnReader::select_with_dictionary_filter(const
SelectionVector& selection,
+ uint16_t
selected_rows, int64_t batch_rows,
+ const
IColumn::Filter& dictionary_filter,
+ MutableColumnPtr&
column,
+ IColumn::Filter*
row_filter,
+ bool* used_filter) {
+ DORIS_CHECK(row_filter != nullptr);
+ DORIS_CHECK(used_filter != nullptr);
+ RETURN_IF_ERROR(validate_selected_span(batch_rows));
+ *used_filter = false;
+ row_filter->clear();
+ if (!_dictionary_filter_enabled) {
+ return Status::OK();
+ }
+ *used_filter = true;
+
+ const uint8_t* filter_data = nullptr;
+ RETURN_IF_ERROR(selection.materialize_filter(selected_rows, batch_rows,
&filter_data));
+ const bool nullable = _type->is_nullable();
+ DataTypePtr id_type = std::make_shared<DataTypeInt32>();
+ if (nullable) {
+ id_type = make_nullable(id_type);
+ }
+ if (!_dictionary_id_column) {
+ _dictionary_id_column = id_type->create_column();
+ }
+ _dictionary_id_column->clear();
+ int64_t rows_read = 0;
+ RETURN_IF_ERROR(read_with_filter(batch_rows, filter_data, selected_rows ==
0,
+ _dictionary_id_column, id_type, true,
&rows_read));
+ advance_selected_span(rows_read);
+ if (_dictionary_id_column->size() != selected_rows) {
+ return Status::Corruption(
+ "Native parquet dictionary reader appended {} rows, expected
{} for {}",
+ _dictionary_id_column->size(), selected_rows, _name);
+ }
+
+ const ColumnInt32* ids = nullptr;
+ const NullMap* null_map = nullptr;
+ if (const auto* nullable_ids =
check_and_get_column<ColumnNullable>(*_dictionary_id_column);
+ nullable_ids != nullptr) {
+ ids =
check_and_get_column<ColumnInt32>(nullable_ids->get_nested_column());
+ null_map = &nullable_ids->get_null_map_data();
+ } else {
+ ids = check_and_get_column<ColumnInt32>(*_dictionary_id_column);
+ }
+ DORIS_CHECK(ids != nullptr);
+
+ if (!_matched_dictionary_ids) {
+ _matched_dictionary_ids = ColumnInt32::create();
+ }
+ _matched_dictionary_ids->clear();
+ auto& matched_ids =
assert_cast<ColumnInt32&>(*_matched_dictionary_ids).get_data();
+ row_filter->reserve(selected_rows);
+ const auto& id_data = ids->get_data();
+ for (size_t row = 0; row < selected_rows; ++row) {
+ bool keep = false;
+ if (null_map == nullptr || (*null_map)[row] == 0) {
+ const int32_t dictionary_id = id_data[row];
+ if (dictionary_id < 0 ||
+ static_cast<size_t>(dictionary_id) >=
dictionary_filter.size()) {
+ return Status::Corruption(
+ "Invalid parquet dictionary id {} for column {} with
{} entries",
+ dictionary_id, _name, dictionary_filter.size());
+ }
+ keep = dictionary_filter[static_cast<size_t>(dictionary_id)] != 0;
+ if (keep) {
+ matched_ids.push_back(dictionary_id);
+ }
+ }
+ row_filter->push_back(keep ? 1 : 0);
+ }
+
+ const auto* matched_id_column =
check_and_get_column<ColumnInt32>(*_matched_dictionary_ids);
+ DORIS_CHECK(matched_id_column != nullptr);
+ auto string_values =
+
DORIS_TRY(_native_reader->convert_dict_column_to_string_column(matched_id_column));
+ RETURN_IF_ERROR(append_non_null_dictionary_values(column,
std::move(string_values)));
+ if (_profile.reader_select_rows != nullptr) {
+ COUNTER_UPDATE(_profile.reader_select_rows, selected_rows);
+ }
+ update_reader_read_rows(cast_set<int64_t>(matched_ids.size()));
+ update_reader_skip_rows(batch_rows -
cast_set<int64_t>(matched_ids.size()));
+ return Status::OK();
+}
+
+void NativeColumnReader::flush_profile() {
+ int64_t max_leaf_page_reads = 0;
+ record_page_fragments(sync_native_profile(&max_leaf_page_reads),
max_leaf_page_reads);
+}
+
+Result<MutableColumnPtr> NativeColumnReader::dictionary_values() {
+ DORIS_CHECK(_native_reader != nullptr);
+ return _native_reader->dictionary_values();
+}
+
+void NativeColumnReader::record_page_fragments(int64_t page_fragments,
+ int64_t max_leaf_page_reads) {
+ if (_profile.native_page_fragments != nullptr) {
+ COUNTER_UPDATE(_profile.native_page_fragments, page_fragments);
+ }
+ // Summed fragments from MAP/STRUCT siblings do not mean any individual
leaf crossed a page.
+ if (max_leaf_page_reads > 1 && _profile.page_crossing_batches != nullptr) {
Review Comment:
[P2] Count page crossings at the scheduler batch boundary `flush_profile()`
runs only every 16 scheduler batches (or at the row-group tail), so
`max_leaf_page_reads > 1` describes an amortized interval, not one batch.
Sixteen batches that each open one page become one false crossing, sixteen real
crossings become one, and each projected top-level column can increment the
same shared counter. This makes `PageCrossingBatches` incomparable with
per-batch `TotalBatches` as documented. Record/OR a crossing flag for each
scheduler batch and keep the periodic flush only for additive cumulative
statistics.
##########
be/src/format_v2/parquet/reader/native/delta_bit_pack_decoder.h:
##########
@@ -0,0 +1,669 @@
+// 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.
+
+#pragma once
+
+#include <gen_cpp/parquet_types.h>
+#include <glog/logging.h>
+
+#include <cstddef>
+#include <cstdint>
+#include <cstring>
+#include <limits>
+#include <memory>
+#include <ostream>
+#include <string>
+#include <type_traits>
+#include <utility>
+#include <vector>
+
+#include "common/status.h"
+#include "core/column/column_fixed_length_object.h"
+#include "core/column/column_vector.h"
+#include "core/data_type/data_type.h"
+#include "exec/common/arithmetic_overflow.h"
+#include "format_v2/parquet/reader/native/decoder.h"
+#include "util/bit_stream_utils.h"
+#include "util/bit_stream_utils.inline.h"
+#include "util/slice.h"
+
+namespace doris::format::parquet::native {
+class DeltaDecoder : public Decoder {
+public:
+ DeltaDecoder() = default;
+ ~DeltaDecoder() override = default;
+};
+
+/**
+ * Format
+ * [header] [block 1] [block 2] ... [block N]
+ * Header
+ * [block size] [_mini_blocks_per_block] [_total_value_count] [first
value]
+ * Block
+ * [min delta] [list of bitwidths of the mini blocks] [miniblocks]
+ */
+template <typename T>
+class DeltaBitPackDecoder final : public DeltaDecoder {
+public:
+ using UT = std::make_unsigned_t<T>;
+
+ DeltaBitPackDecoder() = default;
+ ~DeltaBitPackDecoder() override = default;
+
+ Status skip_values(size_t num_values) override {
+ constexpr size_t kSkipBatchSize = 4096;
+ _values.resize(std::min(num_values, kSkipBatchSize));
+ size_t skipped = 0;
+ while (skipped < num_values) {
+ const size_t batch_size = std::min(num_values - skipped,
kSkipBatchSize);
+ uint32_t num_valid_values = 0;
+ RETURN_IF_ERROR(_get_internal(_values.data(),
static_cast<uint32_t>(batch_size),
+ &num_valid_values));
+ // Skips retain exact validation without allocating in proportion
to a sparse gap.
+ if (UNLIKELY(num_valid_values != batch_size)) {
+ return Status::IOError("Expected to skip {} Parquet delta
values, skipped {}",
+ num_values, skipped + num_valid_values);
+ }
+ skipped += batch_size;
+ }
+ return Status::OK();
+ }
+
+ Status decode_fixed_values(size_t num_values, ParquetFixedValueConsumer&
consumer) override {
+ _values.resize(num_values);
+ uint32_t decoded_count = 0;
+ RETURN_IF_ERROR(
+ _get_internal(_values.data(), cast_set<uint32_t>(num_values),
&decoded_count));
+ if (UNLIKELY(decoded_count != num_values)) {
+ return Status::IOError("Expected {} Parquet delta values, decoded
{}", num_values,
+ decoded_count);
+ }
+ return consumer.consume(reinterpret_cast<const
uint8_t*>(_values.data()), num_values,
+ sizeof(T));
+ }
+
+ Status decode_selected_fixed_values(const ParquetSelection& selection,
+ ParquetFixedValueConsumer& consumer)
override {
+ _values.resize(selection.total_values);
+ uint32_t decoded_count = 0;
+ RETURN_IF_ERROR(_get_internal(_values.data(),
cast_set<uint32_t>(selection.total_values),
+ &decoded_count));
+ if (UNLIKELY(decoded_count != selection.total_values)) {
+ return Status::IOError("Expected {} Parquet delta values, decoded
{}",
+ selection.total_values, decoded_count);
+ }
+ size_t output = 0;
+ for (const auto& range : selection.ranges) {
+ memmove(_values.data() + output, _values.data() + range.first,
range.count * sizeof(T));
+ output += range.count;
+ }
+ DORIS_CHECK_EQ(output, selection.selected_values);
+ return consumer.consume(reinterpret_cast<const
uint8_t*>(_values.data()), output,
+ sizeof(T));
+ }
+
+ Status decode(T* buffer, uint32_t num_values, uint32_t* out_num_values) {
+ return _get_internal(buffer, num_values, out_num_values);
+ }
+
+ uint32_t valid_values_count() {
+ // _total_value_count in header ignores of null values
+ return _total_values_remaining;
+ }
+
+ void release_scratch(size_t max_retained_bytes) override {
+ release_vector_if_oversized(&_values, max_retained_bytes);
+ release_vector_if_oversized(&_delta_bit_widths, max_retained_bytes);
+ }
+ size_t retained_scratch_bytes() const override {
+ return _values.capacity() * sizeof(T) + _delta_bit_widths.capacity() *
sizeof(uint8_t);
+ }
+ size_t active_scratch_bytes() const override {
+ return _values.size() * sizeof(T) + _delta_bit_widths.size() *
sizeof(uint8_t);
+ }
+
+ Status set_data(Slice* slice) override {
+ _bit_reader.reset(
+ new BitReader((const uint8_t*)slice->data,
cast_set<uint32_t>(slice->size)));
+ RETURN_IF_ERROR(_init_header());
+ _data = slice;
+ _offset = 0;
+ return Status::OK();
+ }
+
+ // Set BitReader which is already initialized by
DeltaLengthByteArrayDecoder or
+ // DeltaByteArrayDecoder
+ Status set_bit_reader(std::shared_ptr<BitReader> bit_reader) {
+ _bit_reader = std::move(bit_reader);
+ RETURN_IF_ERROR(_init_header());
+ return Status::OK();
+ }
+
+private:
+ static constexpr int kMaxDeltaBitWidth = static_cast<int>(sizeof(T) * 8);
+ Status _init_header();
+ Status _init_block();
+ Status _init_mini_block(int bit_width);
+ Status _get_internal(T* buffer, uint32_t max_values, uint32_t*
out_num_values);
+
+ std::vector<T> _values;
+
+ std::shared_ptr<BitReader> _bit_reader;
+ uint32_t _values_per_block;
+ uint32_t _mini_blocks_per_block;
+ uint32_t _values_per_mini_block;
+ uint32_t _total_value_count;
+
+ T _min_delta;
+ T _last_value;
+
+ uint32_t _mini_block_idx;
+ std::vector<uint8_t> _delta_bit_widths;
+ int _delta_bit_width;
+ // If the page doesn't contain any block, `_block_initialized` will
+ // always be false. Otherwise, it will be true when first block
initialized.
+ bool _block_initialized;
+
+ uint32_t _total_values_remaining;
+ // Remaining values in current mini block. If the current block is the
last mini block,
+ // _values_remaining_current_mini_block may greater than
_total_values_remaining.
+ uint32_t _values_remaining_current_mini_block;
+};
+
+class DeltaLengthByteArrayDecoder final : public DeltaDecoder {
+public:
+ explicit DeltaLengthByteArrayDecoder()
+ : _len_decoder(), _buffered_length(0), _buffered_data(0) {}
+
+ void set_expected_values(size_t expected_values) override {
+ Decoder::set_expected_values(expected_values);
+ _len_decoder.set_expected_values(expected_values);
+ }
+
+ Status skip_values(size_t num_values) override {
+ constexpr size_t kSkipBatchSize = 4096;
+ _values.resize(std::min(num_values, kSkipBatchSize));
+ size_t skipped = 0;
+ while (skipped < num_values) {
+ const size_t batch_size = std::min(num_values - skipped,
kSkipBatchSize);
+ int num_valid_values = 0;
+ RETURN_IF_ERROR(
+ _get_internal(_values.data(),
static_cast<int>(batch_size), &num_valid_values));
+ if (UNLIKELY(num_valid_values != batch_size)) {
+ return Status::IOError(
+ "Expected to skip {} Parquet delta-length values,
skipped {}", num_values,
+ skipped + num_valid_values);
+ }
+ skipped += batch_size;
+ }
+ return Status::OK();
+ }
+
+ Status decode_binary_values(size_t num_values, ParquetBinaryValueConsumer&
consumer) override {
+ _values.resize(num_values);
+ int decoded_count = 0;
+ RETURN_IF_ERROR(
+ _get_internal(_values.data(), cast_set<int32_t>(num_values),
&decoded_count));
+ if (UNLIKELY(decoded_count != num_values)) {
+ return Status::IOError("Expected {} Parquet delta-length values,
decoded {}",
+ num_values, decoded_count);
+ }
+ _string_refs.resize(num_values);
+ for (size_t row = 0; row < num_values; ++row) {
+ _string_refs[row] = StringRef(_values[row].data,
_values[row].size);
+ }
+ return consumer.consume(_string_refs.data(), _string_refs.size());
+ }
+
+ Status decode_selected_binary_values(const ParquetSelection& selection,
+ ParquetBinaryValueConsumer& consumer)
override {
+ _values.resize(selection.total_values);
+ int decoded_count = 0;
+ RETURN_IF_ERROR(_get_internal(_values.data(),
cast_set<int32_t>(selection.total_values),
+ &decoded_count));
+ if (UNLIKELY(decoded_count != selection.total_values)) {
+ return Status::IOError("Expected {} Parquet delta-length values,
decoded {}",
+ selection.total_values, decoded_count);
+ }
+ _string_refs.resize(selection.selected_values);
+ size_t output = 0;
+ for (const auto& range : selection.ranges) {
+ for (size_t row = 0; row < range.count; ++row) {
+ const auto& value = _values[range.first + row];
+ _string_refs[output++] = StringRef(value.data, value.size);
+ }
+ }
+ DORIS_CHECK_EQ(output, selection.selected_values);
+ return consumer.consume(_string_refs.data(), _string_refs.size());
+ }
+
+ Status decode(Slice* buffer, int num_values, int* out_num_values) {
+ return _get_internal(buffer, num_values, out_num_values);
+ }
+
+ int valid_values_count() const { return _num_valid_values; }
+
+ void release_scratch(size_t max_retained_bytes) override {
+ release_vector_if_oversized(&_values, max_retained_bytes);
+ release_vector_if_oversized(&_string_refs, max_retained_bytes);
+ release_vector_if_oversized(&_buffered_length, max_retained_bytes);
+ release_vector_if_oversized(&_buffered_data, max_retained_bytes);
+ _len_decoder.release_scratch(max_retained_bytes);
+ }
+ size_t retained_scratch_bytes() const override {
+ return _values.capacity() * sizeof(Slice) + _string_refs.capacity() *
sizeof(StringRef) +
+ _buffered_length.capacity() * sizeof(int32_t) +
+ _buffered_data.capacity() * sizeof(char) +
_len_decoder.retained_scratch_bytes();
+ }
+ size_t active_scratch_bytes() const override {
+ return _values.size() * sizeof(Slice) + _string_refs.size() *
sizeof(StringRef) +
+ _buffered_length.size() * sizeof(int32_t) +
_buffered_data.size() * sizeof(char) +
+ _len_decoder.active_scratch_bytes();
+ }
+
+ Status set_data(Slice* slice) override {
+ if (slice == nullptr || slice->size == 0) {
+ // Reused decoders must never retain lengths or payload pointers
from the prior page.
+ _bit_reader.reset();
+ _data = nullptr;
+ _offset = 0;
+ _num_valid_values = 0;
+ _buffered_length.clear();
+ _buffered_data.clear();
+ return Status::Corruption("Parquet delta-length page is empty");
+ }
+ _bit_reader = std::make_shared<BitReader>((const uint8_t*)slice->data,
slice->size);
+ _data = slice;
+ _offset = 0;
+ RETURN_IF_ERROR(_init_lengths());
+ return Status::OK();
+ }
+
+ Status set_bit_reader(std::shared_ptr<BitReader> bit_reader) {
+ _bit_reader = std::move(bit_reader);
+ RETURN_IF_ERROR(_init_lengths());
+ return Status::OK();
+ }
+
+private:
+ Status _init_lengths();
+ Status _get_internal(Slice* buffer, int max_values, int* out_num_values);
+
+ std::vector<Slice> _values;
+ std::vector<StringRef> _string_refs;
+ std::shared_ptr<BitReader> _bit_reader;
+ DeltaBitPackDecoder<int32_t> _len_decoder;
+
+ int _num_valid_values;
+ std::vector<int32_t> _buffered_length;
+ std::vector<char> _buffered_data;
+};
+
+class DeltaByteArrayDecoder : public DeltaDecoder {
+public:
+ explicit DeltaByteArrayDecoder() : _buffered_prefix_length(0),
_buffered_data(0) {}
+
+ void set_expected_values(size_t expected_values) override {
+ Decoder::set_expected_values(expected_values);
+ _prefix_len_decoder.set_expected_values(expected_values);
+ _suffix_decoder.set_expected_values(expected_values);
+ }
+
+ Status skip_values(size_t num_values) override {
+ constexpr size_t kSkipBatchSize = 4096;
+ size_t skipped = 0;
+ while (skipped < num_values) {
+ const size_t batch_size = std::min(num_values - skipped,
kSkipBatchSize);
+ RETURN_IF_ERROR(_decode_slices(batch_size));
+ RETURN_IF_ERROR(_validate_fixed_width_values());
+ skipped += batch_size;
+ }
+ return Status::OK();
+ }
+
+ Status decode_binary_values(size_t num_values, ParquetBinaryValueConsumer&
consumer) override {
+ RETURN_IF_ERROR(_decode_slices(num_values));
+ _string_refs.resize(num_values);
+ for (size_t row = 0; row < num_values; ++row) {
+ _string_refs[row] = StringRef(_values[row].data,
_values[row].size);
+ }
+ return consumer.consume(_string_refs.data(), _string_refs.size());
+ }
+
+ Status decode_selected_binary_values(const ParquetSelection& selection,
+ ParquetBinaryValueConsumer& consumer)
override {
+ RETURN_IF_ERROR(_decode_slices(selection.total_values));
+ _string_refs.resize(selection.selected_values);
+ size_t output = 0;
+ for (const auto& range : selection.ranges) {
+ for (size_t row = 0; row < range.count; ++row) {
+ const auto& value = _values[range.first + row];
+ _string_refs[output++] = StringRef(value.data, value.size);
+ }
+ }
+ DORIS_CHECK_EQ(output, selection.selected_values);
+ return consumer.consume(_string_refs.data(), _string_refs.size());
+ }
+
+ Status decode_fixed_values(size_t num_values, ParquetFixedValueConsumer&
consumer) override {
+ RETURN_IF_ERROR(_decode_slices(num_values));
+ RETURN_IF_ERROR(_validate_fixed_width_values());
+ const size_t byte_size = num_values *
static_cast<size_t>(_type_length);
+ _fixed_values.resize(byte_size);
+ for (size_t row = 0; row < num_values; ++row) {
+ memcpy(_fixed_values.data() + row * _type_length,
_values[row].data, _type_length);
+ }
+ return consumer.consume(_fixed_values.data(), num_values,
+ static_cast<size_t>(_type_length));
+ }
+
+ Status decode_selected_fixed_values(const ParquetSelection& selection,
+ ParquetFixedValueConsumer& consumer)
override {
+ RETURN_IF_ERROR(_decode_slices(selection.total_values));
+ // Validate every consumed value, including filtered ranges, so
selection cannot hide a
+ // malformed FIXED_LEN_BYTE_ARRAY width that a later cursor advance
has already committed.
+ RETURN_IF_ERROR(_validate_fixed_width_values());
+ DORIS_CHECK(_type_length > 0);
+ const size_t value_width = static_cast<size_t>(_type_length);
+ if (UNLIKELY(selection.selected_values >
+ std::numeric_limits<size_t>::max() / value_width)) {
+ return Status::IOError("Parquet delta-byte-array selection byte
size overflows");
+ }
+ _fixed_values.resize(selection.selected_values * value_width);
+ size_t output = 0;
+ for (const auto& range : selection.ranges) {
+ for (size_t row = 0; row < range.count; ++row) {
+ const auto& value = _values[range.first + row];
+ memcpy(_fixed_values.data() + output * value_width,
value.data, value_width);
+ ++output;
+ }
+ }
+ DORIS_CHECK_EQ(output, selection.selected_values);
+ return consumer.consume(_fixed_values.data(), output, value_width);
+ }
+
+ Status set_data(Slice* slice) override {
+ _bit_reader = std::make_shared<BitReader>((const uint8_t*)slice->data,
slice->size);
+ auto prefix_reader = std::make_shared<BitReader>(*_bit_reader);
+ RETURN_IF_ERROR(_prefix_len_decoder.set_bit_reader(prefix_reader));
+
+ // get the number of encoded prefix lengths
+ int num_prefix = _prefix_len_decoder.valid_values_count();
+ DeltaBitPackDecoder<int32_t> prefix_locator;
+ prefix_locator.set_expected_values(_expected_values);
+ RETURN_IF_ERROR(prefix_locator.set_bit_reader(_bit_reader));
+ constexpr size_t kLocateBatchSize = 4096;
+ std::vector<int32_t> ignored_prefixes(
+ std::min<size_t>(static_cast<size_t>(num_prefix),
kLocateBatchSize));
+ size_t located = 0;
+ while (located < static_cast<size_t>(num_prefix)) {
+ const size_t batch_size =
+ std::min(static_cast<size_t>(num_prefix) - located,
kLocateBatchSize);
+ uint32_t decoded = 0;
+ RETURN_IF_ERROR(prefix_locator.decode(ignored_prefixes.data(),
+
static_cast<uint32_t>(batch_size), &decoded));
+ if (UNLIKELY(decoded != batch_size)) {
+ return Status::Corruption("Parquet delta prefix stream ended
early");
+ }
+ located += batch_size;
+ }
+ _num_valid_values = num_prefix;
+
+ // at this time, the decoder_ will be at the start of the encoded
suffix data.
+ RETURN_IF_ERROR(_suffix_decoder.set_bit_reader(_bit_reader));
+ if (UNLIKELY(_suffix_decoder.valid_values_count() != num_prefix)) {
+ return Status::Corruption("Parquet delta prefix and suffix counts
differ");
+ }
+
+ // TODO: read corrupted files written with bug(PARQUET-246).
_last_value should be set
+ // to _last_value_in_previous_page when decoding a new page(except the
first page)
+ _last_value = "";
+ return Status::OK();
+ }
+
+ Status decode(Slice* buffer, int num_values, int* out_num_values) {
+ return _get_internal(buffer, num_values, out_num_values);
+ }
+
+ void release_scratch(size_t max_retained_bytes) override {
+ release_vector_if_oversized(&_values, max_retained_bytes);
+ release_vector_if_oversized(&_string_refs, max_retained_bytes);
+ release_vector_if_oversized(&_fixed_values, max_retained_bytes);
+ release_vector_if_oversized(&_buffered_prefix_length,
max_retained_bytes);
+ release_vector_if_oversized(&_buffered_data, max_retained_bytes);
+ _prefix_len_decoder.release_scratch(max_retained_bytes);
+ _suffix_decoder.release_scratch(max_retained_bytes);
+ if (_last_value.capacity() > max_retained_bytes)
std::string().swap(_last_value);
+ if (_last_value_in_previous_page.capacity() > max_retained_bytes) {
+ std::string().swap(_last_value_in_previous_page);
+ }
+ }
+ size_t retained_scratch_bytes() const override {
+ return _values.capacity() * sizeof(Slice) + _string_refs.capacity() *
sizeof(StringRef) +
+ _fixed_values.capacity() * sizeof(uint8_t) +
+ _buffered_prefix_length.capacity() * sizeof(int32_t) +
+ _buffered_data.capacity() * sizeof(char) +
_last_value.capacity() +
+ _last_value_in_previous_page.capacity() +
+ _prefix_len_decoder.retained_scratch_bytes() +
+ _suffix_decoder.retained_scratch_bytes();
+ }
+ size_t active_scratch_bytes() const override {
+ return _values.size() * sizeof(Slice) + _string_refs.size() *
sizeof(StringRef) +
+ _fixed_values.size() * sizeof(uint8_t) +
+ _buffered_prefix_length.size() * sizeof(int32_t) +
+ _buffered_data.size() * sizeof(char) + _last_value.size() +
+ _last_value_in_previous_page.size() +
_prefix_len_decoder.active_scratch_bytes() +
+ _suffix_decoder.active_scratch_bytes();
+ }
+
+private:
+ Status _validate_fixed_width_values() const {
+ if (_type_length <= 0) {
+ return Status::OK();
+ }
+ const size_t value_width = static_cast<size_t>(_type_length);
+ for (const auto& value : _values) {
+ if (UNLIKELY(value.size != value_width)) {
+ return Status::Corruption("Parquet fixed value has length {},
expected {}",
+ value.size, value_width);
+ }
+ }
+ return Status::OK();
+ }
+
+ Status _decode_slices(size_t num_values) {
+ _values.resize(num_values);
+ int decoded_count = 0;
+ RETURN_IF_ERROR(
+ _get_internal(_values.data(), cast_set<int32_t>(num_values),
&decoded_count));
+ if (UNLIKELY(decoded_count != num_values)) {
+ return Status::IOError("Expected {} Parquet delta-byte-array
values, decoded {}",
+ num_values, decoded_count);
+ }
+ return Status::OK();
+ }
+
+ Status _get_internal(Slice* buffer, int max_values, int* out_num_values);
+
+ std::vector<Slice> _values;
+ std::vector<StringRef> _string_refs;
+ std::vector<uint8_t> _fixed_values;
+ std::shared_ptr<BitReader> _bit_reader;
+ DeltaBitPackDecoder<int32_t> _prefix_len_decoder;
+ DeltaLengthByteArrayDecoder _suffix_decoder;
+ std::string _last_value;
+ // string buffer for last value in previous page
+ std::string _last_value_in_previous_page;
+ int _num_valid_values;
+ std::vector<int32_t> _buffered_prefix_length;
+ std::vector<char> _buffered_data;
+};
+} // namespace doris::format::parquet::native
+
+namespace doris::format::parquet::native {
+
+template <typename T>
+Status DeltaBitPackDecoder<T>::_init_header() {
+ if (!_bit_reader->GetVlqInt(&_values_per_block) ||
+ !_bit_reader->GetVlqInt(&_mini_blocks_per_block) ||
+ !_bit_reader->GetVlqInt(&_total_value_count) ||
+ !_bit_reader->GetZigZagVlqInt(&_last_value)) {
+ return Status::IOError("Init header eof");
+ }
+ if (_values_per_block == 0) {
+ return Status::InvalidArgument("Cannot have zero value per block");
+ }
+ if (_values_per_block % 128 != 0) {
+ return Status::InvalidArgument(
+ "the number of values in a block must be multiple of 128, but
it's " +
+ std::to_string(_values_per_block));
+ }
+ if (_mini_blocks_per_block == 0) {
+ return Status::InvalidArgument("Cannot have zero miniblock per block");
+ }
+ _values_per_mini_block = _values_per_block / _mini_blocks_per_block;
+ if (_values_per_mini_block == 0) {
+ return Status::InvalidArgument("Cannot have zero value per miniblock");
+ }
+ if (_values_per_mini_block % 32 != 0) {
+ return Status::InvalidArgument(
+ "The number of values in a miniblock must be multiple of 32,
but it's " +
+ std::to_string(_values_per_mini_block));
+ }
+ // Encoded counts are external ULEB32 values. Bound them by the page's
advertised logical
+ // values before any vector uses the count; optional levels may only
reduce this upper bound.
+ if (UNLIKELY(_total_value_count > _expected_values)) {
+ return Status::Corruption("Parquet delta header advertises {} values,
page allows {}",
+ _total_value_count, _expected_values);
+ }
+ _total_values_remaining = _total_value_count;
+ _delta_bit_widths.clear();
+ // init as empty property
+ _block_initialized = false;
+ _delta_bit_width = 0;
+ _values_remaining_current_mini_block = 0;
+ return Status::OK();
+}
+
+template <typename T>
+Status DeltaBitPackDecoder<T>::_init_block() {
+ DCHECK_GT(_total_values_remaining, 0) << "InitBlock called at EOF";
+ if (!_bit_reader->GetZigZagVlqInt(&_min_delta)) {
+ return Status::IOError("Init block eof");
+ }
+
+ // One byte follows for each miniblock. Defer allocation until a block is
actually consumed so
+ // a one-value page cannot turn an irrelevant malicious miniblock count
into a large resize.
+ if (UNLIKELY(_mini_blocks_per_block >
static_cast<uint32_t>(_bit_reader->bytes_left()))) {
+ return Status::Corruption("Parquet delta miniblock count {} exceeds
remaining {} bytes",
+ _mini_blocks_per_block,
_bit_reader->bytes_left());
+ }
+ _delta_bit_widths.resize(_mini_blocks_per_block);
+
+ // read the bitwidth of each miniblock
+ uint8_t* bit_width_data = _delta_bit_widths.data();
+ for (uint32_t i = 0; i < _mini_blocks_per_block; ++i) {
+ if (!_bit_reader->GetAligned<uint8_t>(1, bit_width_data + i)) {
+ return Status::IOError("Decode bit-width EOF");
+ }
+ // Note that non-conformant bitwidth entries are allowed by the
Parquet spec
+ // for extraneous miniblocks in the last block (GH-14923), so we check
+ // the bitwidths when actually using them (see InitMiniBlock()).
+ }
+ _mini_block_idx = 0;
+ _block_initialized = true;
+ RETURN_IF_ERROR(_init_mini_block(bit_width_data[0]));
+ return Status::OK();
+}
+
+template <typename T>
+Status DeltaBitPackDecoder<T>::_init_mini_block(int bit_width) {
+ if (bit_width > kMaxDeltaBitWidth) [[unlikely]] {
+ return Status::InvalidArgument("delta bit width larger than integer
bit width");
+ }
+ _delta_bit_width = bit_width;
+ _values_remaining_current_mini_block = _values_per_mini_block;
+ return Status::OK();
+}
+
+template <typename T>
+Status DeltaBitPackDecoder<T>::_get_internal(T* buffer, uint32_t num_values,
+ uint32_t* out_num_values) {
+ num_values = std::min<uint32_t>(num_values, _total_values_remaining);
+ if (num_values == 0) {
+ *out_num_values = 0;
+ return Status::OK();
+ }
+ uint32_t i = 0;
+ while (i < num_values) {
+ if (_values_remaining_current_mini_block == 0) [[unlikely]] {
+ if (!_block_initialized) [[unlikely]] {
+ buffer[i++] = _last_value;
+ DCHECK_EQ(i, 1); // we're at the beginning of the page
+ if (i == num_values) {
+ // When block is uninitialized and i reaches num_values we
have two
+ // different possibilities:
+ // 1. _total_value_count == 1, which means that the page
may have only
+ // one value (encoded in the header), and we should not
initialize
+ // any block.
+ // 2. _total_value_count != 1, which means we should
initialize the
+ // incoming block for subsequent reads.
+ if (_total_value_count != 1) {
+ RETURN_IF_ERROR(_init_block());
+ }
+ break;
+ }
+ RETURN_IF_ERROR(_init_block());
+ } else {
+ ++_mini_block_idx;
+ if (_mini_block_idx < _mini_blocks_per_block) {
+
RETURN_IF_ERROR(_init_mini_block(_delta_bit_widths.data()[_mini_block_idx]));
+ } else {
+ RETURN_IF_ERROR(_init_block());
+ }
+ }
+ }
+
+ uint32_t values_decode =
std::min(_values_remaining_current_mini_block, num_values - i);
+ for (uint32_t j = 0; j < values_decode; ++j) {
+ if (!_bit_reader->GetValue(_delta_bit_width, buffer + i + j)) {
+ return Status::IOError("Get batch EOF");
+ }
+ }
+ for (int j = 0; j < values_decode; ++j) {
+ // Addition between min_delta, packed int and last_value should be
treated as
+ // unsigned addition. Overflow is as expected.
+ buffer[i + j] = static_cast<UT>(_min_delta) +
static_cast<UT>(buffer[i + j]) +
+ static_cast<UT>(_last_value);
+ _last_value = buffer[i + j];
+ }
+ _values_remaining_current_mini_block -= values_decode;
+ i += values_decode;
+ }
+ _total_values_remaining -= num_values;
+
+ if (_total_values_remaining == 0) [[unlikely]] {
+ if (!_bit_reader->Advance(_delta_bit_width *
_values_remaining_current_mini_block)) {
Review Comment:
[P1] Widen the final miniblock padding calculation The multiplication occurs
in `uint32_t` before the result reaches `Advance(int64_t)`. For example, block
size 134217856 with one miniblock, two values, and width 32 requires skipping
4,294,971,360 bits after the second value, but this expression wraps to 4,064
bits. That block still fits the signed page-size domain; in
DELTA_LENGTH_BYTE_ARRAY the shared locator is then 512 MiB early and returns
padding as string payload. Use checked uint64/int64 multiplication, verify it
fits the remaining stream, and cover the overflow boundary plus the byte-array
cursor handoff.
--
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]