This is an automated email from the ASF dual-hosted git repository.
wgtmac pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-cpp.git
The following commit(s) were added to refs/heads/main by this push:
new 310bb055 refactor(parquet): simplify row position handling in
projections (#1020)
310bb055 is described below
commit 310bb055d8f98e6fe31ea103632eb889b7ebc92b
Author: advancedxy <[email protected]>
AuthorDate: Fri Oct 16 14:24:29 2026 +0800
refactor(parquet): simplify row position handling in projections (#1020)
---
src/iceberg/parquet/parquet_data_util.cc | 49 +++++++++++++-----------
src/iceberg/parquet/parquet_data_util_internal.h | 5 ++-
src/iceberg/test/parquet_data_test.cc | 28 ++++++++++++++
3 files changed, 57 insertions(+), 25 deletions(-)
diff --git a/src/iceberg/parquet/parquet_data_util.cc
b/src/iceberg/parquet/parquet_data_util.cc
index 77524aac..449657df 100644
--- a/src/iceberg/parquet/parquet_data_util.cc
+++ b/src/iceberg/parquet/parquet_data_util.cc
@@ -47,6 +47,16 @@ Result<std::shared_ptr<::arrow::Array>> ProjectNestedArray(
const arrow::MetadataColumnContext& metadata_context,
const std::shared_ptr<::arrow::Int64Array>& row_positions,
::arrow::MemoryPool* pool);
+// The reader currently supplies row positions only for top-level projection.
+Result<std::shared_ptr<::arrow::Array>> ProjectNestedArray(
+ const std::shared_ptr<::arrow::Array>& array,
+ const std::shared_ptr<::arrow::DataType>& output_arrow_type,
+ const NestedType& nested_type, std::span<const FieldProjection>
projections,
+ const arrow::MetadataColumnContext& metadata_context, ::arrow::MemoryPool*
pool) {
+ return ProjectNestedArray(array, output_arrow_type, nested_type, projections,
+ metadata_context, /*row_positions=*/nullptr, pool);
+}
+
/// \brief Create a null array of the given type and length.
Result<std::shared_ptr<::arrow::Array>> MakeNullArray(
const std::shared_ptr<::arrow::DataType>& type, int64_t length,
@@ -142,6 +152,9 @@ Result<std::shared_ptr<::arrow::Array>> ProjectStructArray(
"Inconsistent number of fields ({}) and number of output fields ({})",
struct_type.fields().size(), output_struct_type->num_fields());
}
+ ICEBERG_PRECHECK(
+ row_positions != nullptr || !NeedsRowPositions(struct_type,
metadata_context),
+ "Row positions are required to project _pos or inherit _row_id");
std::vector<std::shared_ptr<::arrow::Array>> projected_arrays;
projected_arrays.reserve(projections.size());
@@ -164,12 +177,10 @@ Result<std::shared_ptr<::arrow::Array>>
ProjectStructArray(
if (projected_field.type()->is_nested()) {
const auto& nested_type =
internal::checked_cast<const NestedType&>(*projected_field.type());
- // Metadata columns belong to top-level rows; nested fields need no
positions.
ICEBERG_ASSIGN_OR_RAISE(
projected_array,
ProjectNestedArray(parquet_array, output_arrow_type, nested_type,
- field_projection.children, metadata_context,
- /*row_positions=*/nullptr, pool));
+ field_projection.children, metadata_context,
pool));
} else if
(MetadataColumns::IsRowLineageColumn(projected_field.field_id())) {
ICEBERG_ASSIGN_OR_RAISE(
projected_array,
@@ -255,12 +266,10 @@ Result<std::shared_ptr<::arrow::Array>>
ProjectListArrayImpl(
} else if (element_field.type()->is_nested()) {
const auto& nested_type =
internal::checked_cast<const NestedType&>(*element_field.type());
- // Metadata columns belong to top-level rows; list elements need no
positions.
ICEBERG_ASSIGN_OR_RAISE(
projected_values,
ProjectNestedArray(list_array->values(), output_element_type,
nested_type,
- element_projection.children, metadata_context,
- /*row_positions=*/nullptr, pool));
+ element_projection.children, metadata_context,
pool));
} else {
ICEBERG_ASSIGN_OR_RAISE(
projected_values,
@@ -316,12 +325,10 @@ Result<std::shared_ptr<::arrow::Array>> ProjectMapArray(
ToString(key_projection.kind));
} else if (key_type->is_nested()) {
const auto& nested_type = internal::checked_cast<const
NestedType&>(*key_type);
- // Metadata columns belong to top-level rows; map keys need no positions.
ICEBERG_ASSIGN_OR_RAISE(
projected_keys,
ProjectNestedArray(map_array->keys(), output_map_type->key_type(),
nested_type,
- key_projection.children, metadata_context,
- /*row_positions=*/nullptr, pool));
+ key_projection.children, metadata_context, pool));
} else {
ICEBERG_ASSIGN_OR_RAISE(
projected_keys,
@@ -339,12 +346,10 @@ Result<std::shared_ptr<::arrow::Array>> ProjectMapArray(
ToString(value_projection.kind));
} else if (value_type->is_nested()) {
const auto& nested_type = internal::checked_cast<const
NestedType&>(*value_type);
- // Metadata columns belong to top-level rows; map values need no positions.
ICEBERG_ASSIGN_OR_RAISE(
projected_items,
ProjectNestedArray(map_array->items(), output_map_type->item_type(),
nested_type,
- value_projection.children, metadata_context,
- /*row_positions=*/nullptr, pool));
+ value_projection.children, metadata_context, pool));
} else {
ICEBERG_ASSIGN_OR_RAISE(
projected_items,
@@ -419,9 +424,9 @@ Result<std::shared_ptr<::arrow::Array>> ProjectNestedArray(
} // namespace
-bool NeedsRowPositions(const Schema& read_schema,
+bool NeedsRowPositions(const StructType& struct_type,
const arrow::MetadataColumnContext& metadata_context) {
- return std::ranges::any_of(read_schema.fields(), [&](const SchemaField&
field) {
+ return std::ranges::any_of(struct_type.fields(), [&](const SchemaField&
field) {
return field.field_id() == MetadataColumns::kFilePositionColumnId ||
(field.field_id() == MetadataColumns::kRowIdColumnId &&
metadata_context.first_row_id.has_value());
@@ -435,15 +440,13 @@ Result<std::shared_ptr<::arrow::RecordBatch>>
ProjectRecordBatch(
const arrow::MetadataColumnContext& metadata_context,
const std::shared_ptr<::arrow::Int64Array>& row_positions,
::arrow::MemoryPool* pool) {
- if (row_positions == nullptr && NeedsRowPositions(projected_schema,
metadata_context)) {
- return InvalidArgument(
- "Row positions are required to project _pos or inherit _row_id");
- }
- if (row_positions != nullptr && (row_positions->length() !=
record_batch->num_rows() ||
- row_positions->null_count() != 0)) {
- return InvalidArgument("Expected {} non-null row positions",
- record_batch->num_rows());
- }
+ ICEBERG_PRECHECK(
+ row_positions != nullptr || !NeedsRowPositions(projected_schema,
metadata_context),
+ "Row positions are required to project _pos or inherit _row_id");
+ ICEBERG_PRECHECK(
+ row_positions == nullptr || (row_positions->length() ==
record_batch->num_rows() &&
+ row_positions->null_count() == 0),
+ "Expected {} non-null row positions", record_batch->num_rows());
auto array = std::make_shared<::arrow::StructArray>(
::arrow::struct_(record_batch->schema()->fields()),
record_batch->num_rows(),
record_batch->columns());
diff --git a/src/iceberg/parquet/parquet_data_util_internal.h
b/src/iceberg/parquet/parquet_data_util_internal.h
index 77bb5645..ad70c223 100644
--- a/src/iceberg/parquet/parquet_data_util_internal.h
+++ b/src/iceberg/parquet/parquet_data_util_internal.h
@@ -26,8 +26,8 @@
namespace iceberg::parquet {
-/// \brief Whether the schema projects _pos, or _row_id with a known
first_row_id.
-bool NeedsRowPositions(const Schema& read_schema,
+/// \brief Whether direct fields need positions for _pos or inherited _row_id.
+bool NeedsRowPositions(const StructType& struct_type,
const arrow::MetadataColumnContext& metadata_context);
/// \brief Convert record batch read from a Parquet file to projected Iceberg
Schema.
@@ -38,6 +38,7 @@ bool NeedsRowPositions(const Schema& read_schema,
/// \param projection The projection from projected Iceberg schema to the
record batch.
/// \param metadata_context The file-level constants for populating metadata
columns.
/// \param row_positions Optional file row positions for _pos and inherited
_row_id.
+/// Currently available only to top-level fields.
/// Required when NeedsRowPositions(projected_schema, metadata_context) is
true.
/// When non-null, must contain one non-null value per row in record_batch.
/// \param pool The arrow memory pool.
diff --git a/src/iceberg/test/parquet_data_test.cc
b/src/iceberg/test/parquet_data_test.cc
index f7072324..1ec4d806 100644
--- a/src/iceberg/test/parquet_data_test.cc
+++ b/src/iceberg/test/parquet_data_test.cc
@@ -160,6 +160,34 @@ TEST(ProjectRecordBatchTest,
RowPositionsRequiredForPhysicalRowIdInheritance) {
IsError(ErrorKind::kInvalidArgument));
}
+TEST(ProjectRecordBatchTest, NestedMetadataRequiresRowPositions) {
+ auto id_field = SchemaField::MakeRequired(2, "id", int32());
+ Schema source_schema({SchemaField::MakeRequired(
+ 1, "nested",
std::make_shared<StructType>(std::vector<SchemaField>{id_field}))});
+ auto nested_type = ::arrow::struct_({::arrow::field("id", ::arrow::int32(),
false)});
+ auto input = RecordBatchFromJson(
+ ::arrow::schema({::arrow::field("nested", nested_type, false)}),
"[[[10]]]");
+
+ for (const auto& field : {MetadataColumns::kRowPosition,
MetadataColumns::kRowId}) {
+ SCOPED_TRACE(field.name());
+ Schema projected_schema({SchemaField::MakeRequired(
+ 1, "nested",
+ std::make_shared<StructType>(std::vector<SchemaField>{id_field,
field}))});
+ ICEBERG_UNWRAP_OR_FAIL(auto projection, Project(projected_schema,
source_schema,
+ /*prune_source=*/false));
+ auto output_type = ::arrow::struct_(
+ {::arrow::field("id", ::arrow::int32(), false),
+ ::arrow::field(std::string(field.name()), ::arrow::int64(),
field.optional())});
+ auto output_schema = ::arrow::schema({::arrow::field("nested",
output_type, false)});
+ // Query engines are expected to project these metadata columns at the top
level.
+ // Construct a nested projection deliberately to exercise the check.
+ EXPECT_THAT(ProjectRecordBatch(input, output_schema, projected_schema,
projection,
+ {.first_row_id = 100}, nullptr,
+ ::arrow::default_memory_pool()),
+ IsError(ErrorKind::kInvalidArgument));
+ }
+}
+
TEST(ProjectRecordBatchTest, LineageWithoutRowPositions) {
Schema data_schema({SchemaField::MakeRequired(1, "id", int32())});
Schema lineage_schema({SchemaField::MakeRequired(1, "id", int32()),