Repository navigation
feat(avro,parquet): read the full file schema when no projection is set #1055
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -62,6 +62,27 @@ Result<std::shared_ptr<::arrow::io::RandomAccessFile>> OpenInputStream( | |
| return arrow::OpenArrowInputStream(options.io, options.path, options.length); | ||
| } | ||
|
|
||
| // SchemaManifest holds pointers into its own fields, so it is filled in place. | ||
| Status MakeSchemaManifest(::parquet::arrow::FileReader* reader, | ||
| const NameMapping* name_mapping, | ||
| ::parquet::arrow::SchemaManifest* schema_manifest) { | ||
| auto metadata = reader->parquet_reader()->metadata(); | ||
|
|
||
| ICEBERG_ARROW_RETURN_NOT_OK(::parquet::arrow::SchemaManifest::Make( | ||
| metadata->schema(), metadata->key_value_metadata(), reader->properties(), | ||
| schema_manifest)); | ||
|
|
||
| if (!HasFieldIds(metadata->schema()->schema_root())) { | ||
| if (name_mapping == nullptr) { | ||
| return InvalidSchema( | ||
| "Parquet file schema has no field IDs and no name mapping provided"); | ||
| } | ||
| ApplyNameMapping(*name_mapping, schema_manifest); | ||
| } | ||
|
|
||
| return {}; | ||
| } | ||
|
|
||
| Result<SchemaProjection> BuildProjection(::parquet::arrow::FileReader* reader, | ||
| const Schema& read_schema, | ||
| const NameMapping* name_mapping) { | ||
|
|
@@ -278,11 +299,6 @@ class ParquetReader::Impl { | |
| public: | ||
| // Open the Parquet reader with the given options | ||
| Status Open(const ReaderOptions& options) { | ||
| if (options.projection == nullptr) { | ||
| return InvalidArgument("Projected schema is required by Parquet reader"); | ||
| } | ||
|
|
||
| read_schema_ = options.projection; | ||
| stats_filter_.reset(); | ||
|
|
||
| // Prepare reader properties | ||
|
|
@@ -304,6 +320,16 @@ class ParquetReader::Impl { | |
| reader_, ::parquet::arrow::FileReader::Make(pool_, std::move(file_reader), | ||
| arrow_reader_properties)); | ||
|
|
||
| ::parquet::arrow::SchemaManifest schema_manifest; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SchemaManifest is built twice, once here, once in BuildProjection, I think we can reduce one. |
||
| ICEBERG_RETURN_UNEXPECTED( | ||
| MakeSchemaManifest(reader_.get(), options.name_mapping.get(), &schema_manifest)); | ||
| if (options.projection != nullptr) { | ||
| read_schema_ = options.projection; | ||
| } else { | ||
| // Read all fields of the file schema when no projection is given. | ||
| ICEBERG_ASSIGN_OR_RAISE(read_schema_, FromParquetSchema(schema_manifest)); | ||
| } | ||
|
|
||
| // Project read schema onto the Parquet file schema | ||
| ICEBERG_ASSIGN_OR_RAISE(projection_, BuildProjection(reader_.get(), *read_schema_, | ||
| options.name_mapping.get())); | ||
|
|
@@ -313,7 +339,7 @@ class ParquetReader::Impl { | |
| ICEBERG_ASSIGN_OR_RAISE(auto is_bound, IsBoundVisitor::IsBound(filter)); | ||
| if (!is_bound) { | ||
| ICEBERG_ASSIGN_OR_RAISE( | ||
| filter, Binder::Bind(*options.projection, filter, options.case_sensitive)); | ||
| filter, Binder::Bind(*read_schema_, filter, options.case_sensitive)); | ||
| } | ||
| ICEBERG_ASSIGN_OR_RAISE(stats_filter_, ParquetMetricsRowGroupFilter::Make( | ||
| filter, *reader_->manifest().descr)); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,13 +17,15 @@ | |
| * under the License. | ||
| */ | ||
|
|
||
| #include <arrow/c/bridge.h> | ||
| #include <arrow/extension_type.h> | ||
| #include <arrow/type.h> | ||
| #include <arrow/type_fwd.h> | ||
| #include <arrow/util/key_value_metadata.h> | ||
| #include <parquet/arrow/schema.h> | ||
| #include <parquet/schema.h> | ||
|
|
||
| #include "iceberg/arrow_c_data_guard_internal.h" | ||
| #include "iceberg/constants.h" | ||
| #include "iceberg/metadata_columns.h" | ||
| #include "iceberg/name_mapping.h" | ||
|
|
@@ -781,6 +783,60 @@ void ApplyNameMappingToField(const NameMapping& mapping, std::vector<std::string | |
| } | ||
| } | ||
|
|
||
| // Rebuilds the Arrow field of a Parquet schema field so that nested fields carry the | ||
| // field IDs stored in the SchemaField tree (e.g. those assigned by a name mapping). | ||
| // List elements and map keys/values use the Iceberg names. Returns nullptr if the field, | ||
| // or any list element or map key/value inside it, has no field ID, since such a field | ||
| // cannot be read by ID. | ||
| std::shared_ptr<::arrow::Field> ToArrowFieldWithIds( | ||
| const ::parquet::arrow::SchemaField& parquet_field) { | ||
| if (!GetFieldId(parquet_field).has_value()) { | ||
| return nullptr; | ||
| } | ||
|
|
||
| const auto& field = parquet_field.field; | ||
| const auto& children = parquet_field.children; | ||
| switch (field->type()->id()) { | ||
| case ::arrow::Type::STRUCT: { | ||
| ::arrow::FieldVector child_fields; | ||
| child_fields.reserve(children.size()); | ||
| for (const auto& child : children) { | ||
| if (auto child_field = ToArrowFieldWithIds(child); child_field != nullptr) { | ||
| child_fields.push_back(std::move(child_field)); | ||
| } | ||
| } | ||
| return field->WithType(::arrow::struct_(std::move(child_fields))); | ||
| } | ||
| case ::arrow::Type::LIST: | ||
| case ::arrow::Type::LARGE_LIST: { | ||
| if (children.size() != 1) { | ||
| return nullptr; | ||
| } | ||
| auto element_field = ToArrowFieldWithIds(children[0]); | ||
| if (element_field == nullptr) { | ||
| return nullptr; | ||
| } | ||
| return field->WithType( | ||
| ::arrow::list(element_field->WithName(std::string(ListType::kElementName)))); | ||
| } | ||
| case ::arrow::Type::MAP: { | ||
| if (children.size() != 1 || children[0].children.size() != 2) { | ||
| return nullptr; | ||
| } | ||
| auto key_field = ToArrowFieldWithIds(children[0].children[0]); | ||
| auto value_field = ToArrowFieldWithIds(children[0].children[1]); | ||
| if (key_field == nullptr || value_field == nullptr) { | ||
| return nullptr; | ||
| } | ||
| return field->WithType(std::make_shared<::arrow::MapType>( | ||
| key_field->WithName(std::string(MapType::kKeyName)), | ||
| value_field->WithName(std::string(MapType::kValueName)))); | ||
| } | ||
| default: | ||
| return field; | ||
| } | ||
| } | ||
|
|
||
| } // namespace | ||
|
|
||
| Result<SchemaProjection> Project(const Schema& expected_schema, | ||
|
|
@@ -826,6 +882,27 @@ void ApplyNameMapping(const NameMapping& mapping, | |
| } | ||
| } | ||
|
|
||
| Result<std::unique_ptr<Schema>> FromParquetSchema( | ||
| const ::parquet::arrow::SchemaManifest& parquet_schema) { | ||
| ::arrow::FieldVector fields; | ||
| fields.reserve(parquet_schema.schema_fields.size()); | ||
| for (const auto& parquet_field : parquet_schema.schema_fields) { | ||
| if (auto field = ToArrowFieldWithIds(parquet_field); field != nullptr) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fields without an ID are skipped here, but Avro fails on missing IDs, should we made them consistent? |
||
| fields.push_back(std::move(field)); | ||
| } | ||
| } | ||
|
|
||
| ArrowSchema arrow_schema; | ||
| if (auto status = | ||
| ::arrow::ExportSchema(::arrow::Schema(std::move(fields)), &arrow_schema); | ||
| !status.ok()) { | ||
| return InvalidSchema("Failed to export Parquet file schema to Arrow: {}", | ||
| status.message()); | ||
| } | ||
| internal::ArrowSchemaGuard guard(&arrow_schema); | ||
| return FromArrowSchema(arrow_schema, /*schema_id=*/std::nullopt); | ||
| } | ||
|
|
||
| Result<std::shared_ptr<::parquet::SchemaDescriptor>> ToParquetSchema( | ||
| const Schema& iceberg_schema) { | ||
| ::parquet::schema::NodeVector fields; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to validate the key is not optional? We had the following in
ToAvroNodeVisitor::Visit(const MapType& type, ::avro::NodePtr* node), it would be better to mirror that here.