fix: optimize shared-shredding read & fix ORC read-size estimation for nested columns (#216)
diff --git a/src/paimon/common/data/shredding/map_shared_shredding_file_reader.cpp b/src/paimon/common/data/shredding/map_shared_shredding_file_reader.cpp
index b7c8409..b1cdf96 100644
--- a/src/paimon/common/data/shredding/map_shared_shredding_file_reader.cpp
+++ b/src/paimon/common/data/shredding/map_shared_shredding_file_reader.cpp
@@ -32,7 +32,6 @@
#include "paimon/common/utils/arrow/mem_utils.h"
#include "paimon/common/utils/arrow/status_utils.h"
#include "paimon/common/utils/checked_cast.h"
-#include "paimon/core/casting/casting_utils.h"
#include "paimon/core/utils/nested_projection_utils.h"
namespace paimon {
@@ -110,6 +109,56 @@
std::vector<SelectedKey> selected_keys_;
};
+Result<std::shared_ptr<arrow::Array>> MaskSinglePhysicalColumn(
+ const std::shared_ptr<arrow::StructArray>& physical_struct_array,
+ const std::shared_ptr<arrow::ListArray>& field_mapping_array,
+ const std::shared_ptr<arrow::Int32Array>& field_mapping_values,
+ const std::shared_ptr<arrow::Array>& physical_column_array, int32_t physical_column_id,
+ int32_t field_id, const std::string& field_name, arrow::MemoryPool* arrow_pool) {
+ int64_t row_count = physical_struct_array->length();
+ if (physical_column_array->length() != row_count) {
+ return Status::Invalid("shared-shredding physical column length does not match row count");
+ }
+ PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Buffer> validity,
+ arrow::AllocateEmptyBitmap(row_count, arrow_pool));
+ int64_t valid_count = 0;
+ for (int64_t row = 0; row < row_count; ++row) {
+ if (physical_struct_array->IsNull(row)) {
+ continue;
+ }
+ if (field_mapping_array->IsNull(row)) {
+ return Status::Invalid(fmt::format(
+ "__field_mapping cannot be null in non-null shared-shredding row for field {}",
+ field_name));
+ }
+ int32_t mapping_offset = field_mapping_array->value_offset(row);
+ int32_t mapping_length = field_mapping_array->value_length(row);
+ if (physical_column_id < 0 || physical_column_id >= mapping_length) {
+ return Status::Invalid("physical column id is out of __field_mapping range");
+ }
+ int32_t mapping_index = mapping_offset + physical_column_id;
+ if (field_mapping_values->IsNull(mapping_index)) {
+ return Status::Invalid("__field_mapping element cannot be null");
+ }
+ if (field_mapping_values->Value(mapping_index) != field_id ||
+ physical_column_array->IsNull(row)) {
+ continue;
+ }
+ arrow::bit_util::SetBit(validity->mutable_data(), row);
+ ++valid_count;
+ }
+
+ // Replace only the top-level validity; offsets, values, and nested children stay shared.
+ std::shared_ptr<arrow::ArrayData> result_data = physical_column_array->data()->Copy();
+ if (result_data->buffers.empty()) {
+ return Status::Invalid("shared-shredding physical column has no validity buffer slot");
+ }
+ int64_t null_count = row_count - valid_count;
+ result_data->buffers[0] = null_count == 0 ? nullptr : std::move(validity);
+ result_data->SetNullCount(null_count);
+ return arrow::MakeArray(std::move(result_data));
+}
+
class DefaultSelectedKeysReadPlan : public MapFieldReadPlan {
public:
DefaultSelectedKeysReadPlan(const std::shared_ptr<arrow::Field>& logical_field,
@@ -386,12 +435,10 @@
std::shared_ptr<arrow::MapArray> overflow_array;
CollectPhysicalColumns(physical_struct_array, &physical_column_name_to_array, &overflow_array);
for (auto& [_, physical_column_array] : physical_column_name_to_array) {
- if (physical_column_array->type_id() == arrow::Type::DICTIONARY) {
- PAIMON_ASSIGN_OR_RAISE(
- physical_column_array,
- CastingUtils::Cast(physical_column_array, logical_map_type_->item_type(),
- arrow::compute::CastOptions::Safe(), arrow_pool));
- }
+ PAIMON_ASSIGN_OR_RAISE(
+ physical_column_array,
+ NestedProjectionUtils::AlignArrayToReadType(
+ physical_column_array, logical_map_type_->item_type(), arrow_pool));
}
std::shared_ptr<arrow::Int32Array> overflow_keys;
@@ -406,12 +453,9 @@
if (!overflow_items) {
return Status::Invalid("__overflow map item array is null");
}
- if (overflow_items->type_id() == arrow::Type::DICTIONARY) {
- PAIMON_ASSIGN_OR_RAISE(
- overflow_items,
- CastingUtils::Cast(overflow_items, logical_map_type_->item_type(),
- arrow::compute::CastOptions::Safe(), arrow_pool));
- }
+ PAIMON_ASSIGN_OR_RAISE(overflow_items,
+ NestedProjectionUtils::AlignArrayToReadType(
+ overflow_items, logical_map_type_->item_type(), arrow_pool));
}
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::unique_ptr<arrow::ArrayBuilder> map_builder_base,
@@ -530,12 +574,9 @@
std::shared_ptr<arrow::MapArray> overflow_array;
CollectPhysicalColumns(physical_struct_array, &physical_column_name_to_array, &overflow_array);
for (auto& [_, physical_column_array] : physical_column_name_to_array) {
- if (physical_column_array->type_id() == arrow::Type::DICTIONARY) {
- PAIMON_ASSIGN_OR_RAISE(
- physical_column_array,
- CastingUtils::Cast(physical_column_array, value_type,
- arrow::compute::CastOptions::Safe(), arrow_pool));
- }
+ PAIMON_ASSIGN_OR_RAISE(physical_column_array,
+ NestedProjectionUtils::AlignArrayToReadType(physical_column_array,
+ value_type, arrow_pool));
}
std::shared_ptr<arrow::Int32Array> overflow_keys;
@@ -550,65 +591,89 @@
if (!overflow_items) {
return Status::Invalid("__overflow map item array is null");
}
- if (overflow_items->type_id() == arrow::Type::DICTIONARY) {
- PAIMON_ASSIGN_OR_RAISE(
- overflow_items,
- CastingUtils::Cast(overflow_items, value_type, arrow::compute::CastOptions::Safe(),
- arrow_pool));
- }
+ PAIMON_ASSIGN_OR_RAISE(overflow_items, NestedProjectionUtils::AlignArrayToReadType(
+ overflow_items, value_type, arrow_pool));
}
- std::unique_ptr<arrow::ArrayBuilder> access_builder_base;
- PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(access_builder_base,
- arrow::MakeBuilder(LogicalField()->type(), arrow_pool));
- if (!access_builder_base || !access_builder_base->type() ||
- access_builder_base->type()->id() != arrow::Type::STRUCT) {
- return Status::Invalid(
- fmt::format("selected-key MAP field {} is not a STRUCT", LogicalField()->name()));
- }
- auto* access_builder = checked_cast<arrow::StructBuilder*>(access_builder_base.get());
- PAIMON_RETURN_NOT_OK_FROM_ARROW(access_builder->Reserve(physical_struct_array->length()));
-
- for (int64_t row = 0; row < physical_struct_array->length(); ++row) {
- if (physical_struct_array->IsNull(row)) {
- PAIMON_RETURN_NOT_OK_FROM_ARROW(access_builder->AppendNull());
+ int64_t row_count = physical_struct_array->length();
+ arrow::ArrayVector selected_key_arrays;
+ selected_key_arrays.reserve(selected_keys_.size());
+ for (int32_t key_index = 0; key_index < selected_keys_type->num_fields(); ++key_index) {
+ const SelectedKey& selected_key = selected_keys_[key_index];
+ if (selected_key.field_id < 0) {
+ PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
+ std::shared_ptr<arrow::Array> null_array,
+ arrow::MakeArrayOfNull(selected_keys_type->field(key_index)->type(), row_count,
+ arrow_pool));
+ selected_key_arrays.push_back(std::move(null_array));
continue;
}
- if (field_mapping_array->IsNull(row)) {
- return Status::Invalid(fmt::format(
- "__field_mapping cannot be null in non-null shared-shredding row for field {}",
- LogicalField()->name()));
- }
- int32_t mapping_offset = field_mapping_array->value_offset(row);
- PAIMON_RETURN_NOT_OK_FROM_ARROW(access_builder->Append());
- for (int32_t key_index = 0; key_index < selected_keys_type->num_fields(); ++key_index) {
- arrow::ArrayBuilder* value_builder = access_builder->field_builder(key_index);
- const SelectedKey& selected_key = selected_keys_[key_index];
+ if (selected_key.candidate_columns.size() == 1 && !selected_key.may_use_overflow) {
+ int32_t physical_column_id = selected_key.candidate_columns[0];
+ std::string physical_column_name =
+ MapSharedShreddingDefine::PhysicalColumnName(physical_column_id);
+ auto physical_column_iter = physical_column_name_to_array.find(physical_column_name);
+ if (physical_column_iter == physical_column_name_to_array.end()) {
+ return Status::Invalid(
+ fmt::format("cannot find selected physical column {} for field {}",
+ physical_column_name, LogicalField()->name()));
+ }
+ const std::shared_ptr<arrow::Array>& physical_column_array =
+ physical_column_iter->second;
+ if (physical_column_array->offset() == 0) {
+ PAIMON_ASSIGN_OR_RAISE(
+ std::shared_ptr<arrow::Array> masked_array,
+ MaskSinglePhysicalColumn(physical_struct_array, field_mapping_array,
+ field_mapping_values, physical_column_array,
+ physical_column_id, selected_key.field_id,
+ LogicalField()->name(), arrow_pool));
+ selected_key_arrays.push_back(std::move(masked_array));
+ continue;
+ } else {
+ return Status::Invalid("paimon only supports arrays with zero offset");
+ }
+ }
+
+ PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
+ std::unique_ptr<arrow::ArrayBuilder> value_builder,
+ arrow::MakeBuilder(selected_keys_type->field(key_index)->type(), arrow_pool));
+ PAIMON_RETURN_NOT_OK_FROM_ARROW(value_builder->Reserve(row_count));
+ for (int64_t row = 0; row < row_count; ++row) {
+ if (physical_struct_array->IsNull(row)) {
+ PAIMON_RETURN_NOT_OK_FROM_ARROW(value_builder->AppendNull());
+ continue;
+ }
+ if (field_mapping_array->IsNull(row)) {
+ return Status::Invalid(fmt::format(
+ "__field_mapping cannot be null in non-null shared-shredding row for field {}",
+ LogicalField()->name()));
+ }
+ int32_t mapping_offset = field_mapping_array->value_offset(row);
bool appended = false;
- if (selected_key.field_id >= 0) {
- for (int32_t physical_column_id : selected_key.candidate_columns) {
- int32_t mapping_index = mapping_offset + physical_column_id;
- if (field_mapping_values->IsNull(mapping_index)) {
- return Status::Invalid("__field_mapping element cannot be null");
- }
- if (field_mapping_values->Value(mapping_index) != selected_key.field_id) {
- continue;
- }
- std::string physical_column_name =
- MapSharedShreddingDefine::PhysicalColumnName(physical_column_id);
- auto physical_column_iter =
- physical_column_name_to_array.find(physical_column_name);
- if (physical_column_iter == physical_column_name_to_array.end()) {
- return Status::Invalid(
- fmt::format("cannot find selected physical column {} for field {}",
- physical_column_name, LogicalField()->name()));
- }
- PAIMON_RETURN_NOT_OK_FROM_ARROW(value_builder->AppendArraySlice(
- *physical_column_iter->second->data(), row, 1));
- appended = true;
- break;
+ for (int32_t physical_column_id : selected_key.candidate_columns) {
+ int32_t mapping_index = mapping_offset + physical_column_id;
+ if (field_mapping_values->IsNull(mapping_index)) {
+ return Status::Invalid("__field_mapping element cannot be null");
}
+ if (field_mapping_values->Value(mapping_index) != selected_key.field_id) {
+ continue;
+ }
+ std::string physical_column_name =
+ MapSharedShreddingDefine::PhysicalColumnName(physical_column_id);
+ auto physical_column_iter =
+ physical_column_name_to_array.find(physical_column_name);
+ if (physical_column_iter == physical_column_name_to_array.end()) {
+ return Status::Invalid(
+ fmt::format("cannot find selected physical column {} for field {}",
+ physical_column_name, LogicalField()->name()));
+ }
+ const std::shared_ptr<arrow::Array>& physical_column_array =
+ physical_column_iter->second;
+ PAIMON_RETURN_NOT_OK_FROM_ARROW(
+ value_builder->AppendArraySlice(*physical_column_array->data(), row, 1));
+ appended = true;
+ break;
}
if (!appended && selected_key.may_use_overflow && overflow_array &&
@@ -630,9 +695,24 @@
PAIMON_RETURN_NOT_OK_FROM_ARROW(value_builder->AppendNull());
}
}
+ std::shared_ptr<arrow::Array> selected_key_array;
+ PAIMON_RETURN_NOT_OK_FROM_ARROW(value_builder->Finish(&selected_key_array));
+ selected_key_arrays.push_back(std::move(selected_key_array));
}
- std::shared_ptr<arrow::Array> result;
- PAIMON_RETURN_NOT_OK_FROM_ARROW(access_builder->Finish(&result));
+
+ std::shared_ptr<arrow::Buffer> parent_validity;
+ int64_t parent_null_count = physical_struct_array->null_count();
+ if (parent_null_count > 0) {
+ if (physical_struct_array->offset() == 0) {
+ parent_validity = physical_struct_array->null_bitmap();
+ } else {
+ return Status::Invalid("paimon only supports arrays with zero offset");
+ }
+ }
+ PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
+ std::shared_ptr<arrow::StructArray> result,
+ arrow::StructArray::Make(selected_key_arrays, selected_keys_type->fields(),
+ std::move(parent_validity), parent_null_count));
return result;
}
@@ -646,14 +726,10 @@
}
auto map_array = checked_pointer_cast<arrow::MapArray>(physical_array);
auto selected_keys_type = checked_pointer_cast<arrow::StructType>(LogicalField()->type());
- auto physical_map_type = checked_pointer_cast<arrow::MapType>(PhysicalReadField()->type());
std::shared_ptr<arrow::Array> items = map_array->items();
- if (items->type_id() == arrow::Type::DICTIONARY) {
- PAIMON_ASSIGN_OR_RAISE(items,
- CastingUtils::Cast(items, physical_map_type->item_type(),
- arrow::compute::CastOptions::Safe(), arrow_pool));
- }
+ PAIMON_ASSIGN_OR_RAISE(items, NestedProjectionUtils::AlignArrayToReadType(
+ items, selected_keys_type->field(0)->type(), arrow_pool));
std::shared_ptr<arrow::Array> keys = map_array->keys();
std::unique_ptr<arrow::ArrayBuilder> access_builder_base;
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(access_builder_base,
diff --git a/src/paimon/common/data/shredding/map_shared_shredding_file_reader_test.cpp b/src/paimon/common/data/shredding/map_shared_shredding_file_reader_test.cpp
index 80f5046..b2dd937 100644
--- a/src/paimon/common/data/shredding/map_shared_shredding_file_reader_test.cpp
+++ b/src/paimon/common/data/shredding/map_shared_shredding_file_reader_test.cpp
@@ -333,6 +333,96 @@
AssertChunkedArrayEquals(expected, actual);
}
+TEST_F(MapSharedShreddingFileReaderTest, TestSelectedKeysStructProjectionSharesValueBuffers) {
+ ASSERT_OK_AND_ASSIGN(auto physical_array, PhysicalArray());
+ auto physical_root = checked_pointer_cast<arrow::StructArray>(physical_array);
+ auto physical_tags =
+ checked_pointer_cast<arrow::StructArray>(physical_root->GetFieldByName("tags"));
+ auto physical_column =
+ physical_tags->GetFieldByName(MapSharedShreddingDefine::PhysicalColumnName(1));
+
+ auto selected_type = arrow::struct_(
+ {arrow::field("a", arrow::int64()), arrow::field("b", arrow::int64()),
+ arrow::field("e", arrow::int64()), arrow::field("missing", arrow::int64())});
+ auto selected_field = arrow::field(
+ "tags", selected_type, /*nullable=*/true,
+ arrow::KeyValueMetadata::Make({DataField::MAP_SELECTED_KEYS}, {"a,b,e,missing"}));
+ ASSERT_OK_AND_ASSIGN(
+ auto field_read_plan,
+ MapFieldReadPlanFactory::CreateSharedSelectedKeysReadPlan(selected_field, TagsMeta()));
+ ASSERT_OK_AND_ASSIGN(auto result,
+ field_read_plan->Materialize(physical_tags, arrow::default_memory_pool()));
+ auto result_struct = checked_pointer_cast<arrow::StructArray>(result);
+
+ auto expected = arrow::ipc::internal::json::ArrayFromJSON(selected_type, R"([
+ [10, 20, null, null],
+ [40, null, null, null],
+ null,
+ [80, null, 70, null]
+ ])")
+ .ValueOrDie();
+ ASSERT_TRUE(expected->Equals(result)) << "Expected:\n"
+ << expected->ToString() << "\nActual:\n"
+ << result->ToString();
+ ASSERT_EQ(physical_column->data()->buffers[1], result_struct->field(1)->data()->buffers[1]);
+ ASSERT_EQ(physical_column->data()->buffers[1], result_struct->field(2)->data()->buffers[1]);
+ ASSERT_NE(physical_column->data()->buffers[0], result_struct->field(1)->data()->buffers[0]);
+ ASSERT_EQ(physical_tags->data()->buffers[0], result_struct->data()->buffers[0]);
+}
+
+TEST_F(MapSharedShreddingFileReaderTest, TestSelectedKeysStructProjectionSharesNestedValueBuffers) {
+ auto item_type = arrow::list(arrow::int64());
+ auto logical_schema =
+ arrow::schema({arrow::field("id", arrow::int32()),
+ arrow::field("tags", arrow::map(arrow::utf8(), item_type))});
+ ASSERT_OK_AND_ASSIGN(auto physical_schema, MapSharedShreddingUtils::LogicalToPhysicalSchema(
+ logical_schema, {{"tags", 1}}));
+ auto physical_array =
+ arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(physical_schema->fields()), R"([
+ [1, [[0], [1, 2], null]],
+ [2, [[1], [3, 4, 5], null]],
+ [3, null],
+ [4, [[0], null, null]]
+ ])")
+ .ValueOrDie();
+ auto physical_root = checked_pointer_cast<arrow::StructArray>(physical_array);
+ auto physical_tags =
+ checked_pointer_cast<arrow::StructArray>(physical_root->GetFieldByName("tags"));
+ auto physical_column =
+ physical_tags->GetFieldByName(MapSharedShreddingDefine::PhysicalColumnName(0));
+
+ MapSharedShreddingFieldMeta meta;
+ meta.name_to_id = {{"a", 0}, {"b", 1}};
+ meta.field_to_columns = {{0, {0}}, {1, {0}}};
+ meta.num_columns = 1;
+ meta.max_row_width = 1;
+ auto selected_type = arrow::struct_({arrow::field("b", item_type)});
+ auto selected_field =
+ arrow::field("tags", selected_type, /*nullable=*/true,
+ arrow::KeyValueMetadata::Make({DataField::MAP_SELECTED_KEYS}, {"b"}));
+ ASSERT_OK_AND_ASSIGN(
+ auto field_read_plan,
+ MapFieldReadPlanFactory::CreateSharedSelectedKeysReadPlan(selected_field, meta));
+ ASSERT_OK_AND_ASSIGN(auto result,
+ field_read_plan->Materialize(physical_tags, arrow::default_memory_pool()));
+ auto result_struct = checked_pointer_cast<arrow::StructArray>(result);
+ auto result_list = checked_pointer_cast<arrow::ListArray>(result_struct->field(0));
+
+ auto expected = arrow::ipc::internal::json::ArrayFromJSON(selected_type, R"([
+ [null],
+ [[3, 4, 5]],
+ null,
+ [null]
+ ])")
+ .ValueOrDie();
+ ASSERT_TRUE(expected->Equals(result)) << "Expected:\n"
+ << expected->ToString() << "\nActual:\n"
+ << result->ToString();
+ ASSERT_EQ(physical_column->data()->buffers[1], result_list->data()->buffers[1]);
+ ASSERT_EQ(physical_column->data()->child_data[0]->buffers[1],
+ result_list->data()->child_data[0]->buffers[1]);
+}
+
TEST_F(MapSharedShreddingFileReaderTest, TestSelectedKeysStructProjectionFromDefaultMap) {
auto map_type = checked_pointer_cast<arrow::MapType>(
arrow::map(arrow::utf8(), arrow::field("value", arrow::int64())));
@@ -674,6 +764,66 @@
AssertChunkedArrayEquals(expected, actual);
}
+TEST_F(MapSharedShreddingFileReaderTest, TestOrcDictionaryEncodedStringListValue) {
+ std::shared_ptr<arrow::Schema> logical_schema = arrow::schema({
+ arrow::field("id", arrow::int32()),
+ arrow::field("tags", arrow::map(arrow::utf8(), arrow::list(arrow::utf8()))),
+ });
+ auto options = options_;
+ std::string format = "orc";
+ options[Options::FILE_FORMAT] = format;
+ options["orc.dictionary-key-size-threshold"] = "1";
+ ASSERT_OK_AND_ASSIGN(auto table_schema,
+ TableSchema::Create(TableSchema::FIRST_SCHEMA_ID, logical_schema,
+ /*partition_keys=*/{}, /*primary_keys=*/{}, options));
+
+ auto dir = UniqueTestDirectory::Create();
+ ASSERT_TRUE(dir);
+ ASSERT_OK_AND_ASSIGN(CoreOptions core_options, CoreOptions::FromMap(options));
+ auto path_factory = CreatePathFactory(dir->Str(), format, core_options);
+ auto compact_manager = std::make_shared<NoopCompactManager>();
+ ASSERT_OK_AND_ASSIGN(
+ auto writer,
+ CreateAppendOnlyWriter(core_options, /*schema_id=*/0, logical_schema,
+ /*write_cols=*/std::nullopt,
+ /*max_sequence_number=*/-1, path_factory, compact_manager));
+ auto batch = CreateBatch(logical_schema, R"([
+ [1, [["a", ["red", "blue"]], ["b", ["blue"]]]],
+ [2, [["c", ["green"]], ["a", ["red", null, "blue"]], ["b", ["blue"]]]],
+ [3, null],
+ [4, [["d", ["yellow"]], ["e", ["blue"]], ["c", [null]], ["a", ["red"]]]]
+ ])");
+ ASSERT_OK(writer->Write(std::move(batch)));
+ ASSERT_OK_AND_ASSIGN(auto inc, writer->PrepareCommit(/*wait_compaction=*/true));
+ ASSERT_OK(writer->Close());
+
+ std::string data_file_path =
+ path_factory->ToPath(inc.GetNewFilesIncrement().NewFiles()[0]->file_name);
+ std::map<std::string, std::string> reader_options = {{"orc.read.enable-lazy-decoding", "true"}};
+ auto reader = WrapReader(OpenFormatReader(data_file_path, format, reader_options),
+ /*selected_keys_str=*/"a,c");
+
+ auto read_metadata = std::make_shared<arrow::KeyValueMetadata>();
+ read_metadata->Append("paimon.map.selected-keys", "a,c");
+ arrow::FieldVector read_fields = logical_schema->fields();
+ read_fields[1] = read_fields[1]->WithMetadata(read_metadata);
+ auto read_schema = ExportSchema(arrow::schema(std::move(read_fields)));
+ ASSERT_OK(reader->SetReadSchema(read_schema.get(), /*predicate=*/nullptr,
+ /*selection_bitmap=*/std::nullopt));
+ ASSERT_OK_AND_ASSIGN(auto actual, ReadResultCollector::CollectResult(reader.get()));
+ std::shared_ptr<arrow::ChunkedArray> expected;
+ ASSERT_TRUE(arrow::ipc::internal::json::ChunkedArrayFromJSON(
+ arrow::struct_(logical_schema->fields()), {R"([
+ [1, [["a", ["red", "blue"]]]],
+ [2, [["a", ["red", null, "blue"]], ["c", ["green"]]]],
+ [3, null],
+ [4, [["a", ["red"]], ["c", [null]]]]
+ ])"},
+ &expected)
+ .ok());
+ AssertChunkedArrayEquals(expected, actual);
+}
+
TEST_F(MapSharedShreddingFileReaderTest, TestReadsRealFormatFile) {
// TODO(lisizhuo.lsz): support other format
auto options = options_;
diff --git a/src/paimon/format/orc/orc_file_batch_reader.cpp b/src/paimon/format/orc/orc_file_batch_reader.cpp
index cd627eb..a201839 100644
--- a/src/paimon/format/orc/orc_file_batch_reader.cpp
+++ b/src/paimon/format/orc/orc_file_batch_reader.cpp
@@ -46,6 +46,16 @@
#include "paimon/format/orc/predicate_converter.h"
namespace paimon::orc {
+namespace {
+
+void CollectAllColumnIds(const ::orc::Type* type, std::vector<uint64_t>* column_ids) {
+ column_ids->push_back(type->getColumnId());
+ for (uint64_t i = 0; i < type->getSubtypeCount(); ++i) {
+ CollectAllColumnIds(type->getSubtype(i), column_ids);
+ }
+}
+
+} // namespace
OrcFileBatchReader::OrcFileBatchReader(std::unique_ptr<::orc::ReaderMetrics>&& reader_metrics,
std::unique_ptr<OrcReaderWrapper>&& reader,
@@ -228,13 +238,15 @@
}
break;
}
- // Do not support partial field recall inside list/map types.
default: {
+ // Partial field recall inside list/map types is unsupported, so the target must match
+ // the complete source subtree. Include the container and every descendant because all
+ // of their streams are recalled by the ORC reader.
if (src_type->toString() != target_type->toString()) {
return Status::Invalid(fmt::format("type mismatch: src {} vs target {}",
src_type->toString(), target_type->toString()));
}
- target_column_ids->push_back(src_type->getColumnId());
+ CollectAllColumnIds(src_type, target_column_ids);
break;
}
}
diff --git a/src/paimon/format/orc/orc_file_batch_reader_test.cpp b/src/paimon/format/orc/orc_file_batch_reader_test.cpp
index 038c93e..c39ba51 100644
--- a/src/paimon/format/orc/orc_file_batch_reader_test.cpp
+++ b/src/paimon/format/orc/orc_file_batch_reader_test.cpp
@@ -368,8 +368,9 @@
OrcFileBatchReader::CreateRowReaderOptions(
src_type.get(), target_type.get(),
/*search_arg=*/nullptr, options, &target_column_ids));
- // Struct IDs (0, 1) not included. Selected: sub1(2), sub2-list(3), sub3(6), col3-map(8).
- ASSERT_EQ(target_column_ids, (std::vector<uint64_t>{2, 3, 6, 8}));
+ // Struct IDs (0, 1) are not included. LIST/MAP containers include their complete
+ // subtrees: sub1(2), sub2(3, 4, 5), sub3(6), col3(8, 9, 10).
+ ASSERT_EQ(target_column_ids, (std::vector<uint64_t>{2, 3, 4, 5, 6, 8, 9, 10}));
}
{
// read with type mismatch in nested field
@@ -485,6 +486,98 @@
}
}
+TEST_F(OrcFileBatchReaderTest, TestCollectTargetColumnIdsPrimitiveList) {
+ std::unique_ptr<::orc::Type> src_type =
+ ::orc::Type::buildTypeFromString("struct<items:array<int>,ignored:string>");
+ std::unique_ptr<::orc::Type> target_type =
+ ::orc::Type::buildTypeFromString("struct<items:array<int>>");
+ std::vector<uint64_t> target_column_ids;
+
+ ASSERT_OK(OrcFileBatchReader::CollectTargetColumnIds(src_type.get(), target_type.get(),
+ &target_column_ids));
+ // root struct(0), items-list(1), element(2), ignored(3)
+ ASSERT_EQ(target_column_ids, (std::vector<uint64_t>{1, 2}));
+}
+
+TEST_F(OrcFileBatchReaderTest, TestCollectTargetColumnIdsDeeplyNestedList) {
+ std::string schema = "struct<items:array<array<struct<value:double,label:string>>>>";
+ std::unique_ptr<::orc::Type> src_type = ::orc::Type::buildTypeFromString(schema);
+ std::unique_ptr<::orc::Type> target_type = ::orc::Type::buildTypeFromString(schema);
+ std::vector<uint64_t> target_column_ids;
+
+ ASSERT_OK(OrcFileBatchReader::CollectTargetColumnIds(src_type.get(), target_type.get(),
+ &target_column_ids));
+ // root struct(0), outer list(1), inner list(2), element struct(3), value(4), label(5)
+ ASSERT_EQ(target_column_ids, (std::vector<uint64_t>{1, 2, 3, 4, 5}));
+}
+
+TEST_F(OrcFileBatchReaderTest, TestCollectTargetColumnIdsPrimitiveMap) {
+ std::unique_ptr<::orc::Type> src_type =
+ ::orc::Type::buildTypeFromString("struct<attributes:map<string,int>,ignored:double>");
+ std::unique_ptr<::orc::Type> target_type =
+ ::orc::Type::buildTypeFromString("struct<attributes:map<string,int>>");
+ std::vector<uint64_t> target_column_ids;
+
+ ASSERT_OK(OrcFileBatchReader::CollectTargetColumnIds(src_type.get(), target_type.get(),
+ &target_column_ids));
+ // root struct(0), attributes-map(1), key(2), value(3), ignored(4)
+ ASSERT_EQ(target_column_ids, (std::vector<uint64_t>{1, 2, 3}));
+}
+
+TEST_F(OrcFileBatchReaderTest, TestCollectTargetColumnIdsDeeplyNestedMap) {
+ std::string schema =
+ "struct<attributes:map<string,array<struct<score:double,tags:array<string>>>>>";
+ std::unique_ptr<::orc::Type> src_type = ::orc::Type::buildTypeFromString(schema);
+ std::unique_ptr<::orc::Type> target_type = ::orc::Type::buildTypeFromString(schema);
+ std::vector<uint64_t> target_column_ids;
+
+ ASSERT_OK(OrcFileBatchReader::CollectTargetColumnIds(src_type.get(), target_type.get(),
+ &target_column_ids));
+ // root struct(0), map(1), key(2), value-list(3), element struct(4), score(5),
+ // tags-list(6), tag element(7)
+ ASSERT_EQ(target_column_ids, (std::vector<uint64_t>{1, 2, 3, 4, 5, 6, 7}));
+}
+
+TEST_F(OrcFileBatchReaderTest, TestCollectTargetColumnIdsStructProjectionWithListAndMap) {
+ std::unique_ptr<::orc::Type> src_type = ::orc::Type::buildTypeFromString(
+ "struct<outer:struct<items:array<int>,plain:double,attributes:map<string,int>>,"
+ "ignored:string>");
+ std::unique_ptr<::orc::Type> target_type = ::orc::Type::buildTypeFromString(
+ "struct<outer:struct<items:array<int>,attributes:map<string,int>>>");
+ std::vector<uint64_t> target_column_ids;
+
+ ASSERT_OK(OrcFileBatchReader::CollectTargetColumnIds(src_type.get(), target_type.get(),
+ &target_column_ids));
+ // root struct(0) and outer struct(1) are not included. Selected: items(2, 3) and
+ // attributes(5, 6, 7). plain(4) and ignored(8) are skipped.
+ ASSERT_EQ(target_column_ids, (std::vector<uint64_t>{2, 3, 5, 6, 7}));
+}
+
+TEST_F(OrcFileBatchReaderTest, TestCollectTargetColumnIdsRejectsPartialListAndMapProjection) {
+ {
+ std::unique_ptr<::orc::Type> src_type =
+ ::orc::Type::buildTypeFromString("struct<items:array<struct<a:int,b:double>>>");
+ std::unique_ptr<::orc::Type> target_type =
+ ::orc::Type::buildTypeFromString("struct<items:array<struct<a:int>>>");
+ std::vector<uint64_t> target_column_ids;
+ ASSERT_NOK_WITH_MSG(OrcFileBatchReader::CollectTargetColumnIds(
+ src_type.get(), target_type.get(), &target_column_ids),
+ "type mismatch");
+ ASSERT_TRUE(target_column_ids.empty());
+ }
+ {
+ std::unique_ptr<::orc::Type> src_type = ::orc::Type::buildTypeFromString(
+ "struct<attributes:map<string,struct<a:int,b:double>>>");
+ std::unique_ptr<::orc::Type> target_type =
+ ::orc::Type::buildTypeFromString("struct<attributes:map<string,struct<a:int>>>");
+ std::vector<uint64_t> target_column_ids;
+ ASSERT_NOK_WITH_MSG(OrcFileBatchReader::CollectTargetColumnIds(
+ src_type.get(), target_type.get(), &target_column_ids),
+ "type mismatch");
+ ASSERT_TRUE(target_column_ids.empty());
+ }
+}
+
TEST_P(OrcFileBatchReaderTest, TestNextBatchSimple) {
std::string file_name = paimon::test::GetDataDir() +
"/orc/append_09.db/append_09/f1=10/bucket-1/"