| /* | |
| * 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 <array> | |
| #include <format> | |
| #include <limits> | |
| #include <optional> | |
| #include <string> | |
| #include <utility> | |
| #include <vector> | |
| #include <arrow/array.h> | |
| #include <arrow/array/builder_binary.h> | |
| #include <arrow/c/bridge.h> | |
| #include <arrow/extension/uuid.h> | |
| #include <arrow/filesystem/filesystem.h> | |
| #include <arrow/json/from_string.h> | |
| #include <arrow/record_batch.h> | |
| #include <arrow/table.h> | |
| #include <arrow/type.h> | |
| #include <arrow/util/compression.h> | |
| #include <arrow/util/key_value_metadata.h> | |
| #include <parquet/arrow/reader.h> | |
| #include <parquet/arrow/writer.h> | |
| #include <parquet/file_reader.h> | |
| #include <parquet/file_writer.h> | |
| #include <parquet/metadata.h> | |
| #include <parquet/schema.h> | |
| #include "iceberg/arrow/arrow_io_internal.h" | |
| #include "iceberg/arrow/arrow_status_internal.h" | |
| #include "iceberg/expression/literal.h" | |
| #include "iceberg/file_reader.h" | |
| #include "iceberg/file_writer.h" | |
| #include "iceberg/metadata_columns.h" | |
| #include "iceberg/name_mapping.h" | |
| #include "iceberg/parquet/parquet_data_util_internal.h" | |
| #include "iceberg/parquet/parquet_register.h" | |
| #include "iceberg/result.h" | |
| #include "iceberg/schema.h" | |
| #include "iceberg/schema_field.h" | |
| #include "iceberg/schema_internal.h" | |
| #include "iceberg/test/matchers.h" | |
| #include "iceberg/test/std_io.h" | |
| #include "iceberg/test/temp_file_test_base.h" | |
| #include "iceberg/type.h" | |
| #include "iceberg/util/checked_cast.h" | |
| #include "iceberg/util/macros.h" | |
| #include "iceberg/util/uuid.h" | |
| namespace iceberg::parquet { | |
| namespace { | |
| Status WriteArray(std::shared_ptr<::arrow::Array> data, Writer& writer) { | |
| ArrowArray arr; | |
| ICEBERG_ARROW_RETURN_NOT_OK(::arrow::ExportArray(*data, &arr)); | |
| ICEBERG_RETURN_UNEXPECTED(writer.Write(&arr)); | |
| return writer.Close(); | |
| } | |
| Status WriteArray(std::shared_ptr<::arrow::Array> data, | |
| const WriterOptions& writer_options) { | |
| ICEBERG_ASSIGN_OR_RAISE( | |
| auto writer, WriterFactoryRegistry::Open(FileFormatType::kParquet, writer_options)); | |
| return WriteArray(data, *writer); | |
| } | |
| Status ReadArray(std::shared_ptr<::arrow::Array>& out, | |
| const ReaderOptions& reader_options, | |
| std::unordered_map<std::string, std::string>* metadata) { | |
| ICEBERG_ASSIGN_OR_RAISE( | |
| auto reader, ReaderFactoryRegistry::Open(FileFormatType::kParquet, reader_options)); | |
| ICEBERG_ASSIGN_OR_RAISE(auto read_data, reader->Next()); | |
| if (!read_data.has_value()) { | |
| out = nullptr; | |
| return {}; | |
| } | |
| auto arrow_c_array = read_data.value(); | |
| ICEBERG_ASSIGN_OR_RAISE(ArrowSchema arrow_schema, reader->Schema()); | |
| ICEBERG_ARROW_ASSIGN_OR_RETURN(out, | |
| ::arrow::ImportArray(&arrow_c_array, &arrow_schema)); | |
| if (metadata) { | |
| ICEBERG_ASSIGN_OR_RAISE(*metadata, reader->Metadata()); | |
| } | |
| return {}; | |
| } | |
| void DoRoundtrip(std::shared_ptr<::arrow::Array> data, std::shared_ptr<Schema> schema, | |
| std::shared_ptr<::arrow::Array>& out) { | |
| std::shared_ptr<FileIO> file_io = arrow::ArrowFileSystemFileIO::MakeMockFileIO(); | |
| const std::string basePath = "base.parquet"; | |
| std::unordered_map<std::string, std::string> metadata = {{"k1", "v1"}, {"k2", "v2"}}; | |
| WriterProperties writer_properties; | |
| writer_properties.Set(WriterProperties::kParquetCompression, | |
| std::string("uncompressed")); | |
| auto writer_data = WriterFactoryRegistry::Open( | |
| FileFormatType::kParquet, {.path = basePath, | |
| .schema = schema, | |
| .io = file_io, | |
| .metadata = metadata, | |
| .properties = std::move(writer_properties)}); | |
| ASSERT_THAT(writer_data, IsOk()) | |
| << "Failed to create writer: " << writer_data.error().message; | |
| auto writer = std::move(writer_data.value()); | |
| ASSERT_THAT(WriteArray(data, *writer), IsOk()); | |
| std::unordered_map<std::string, std::string> read_metadata; | |
| ICEBERG_UNWRAP_OR_FAIL(auto length, writer->length()); | |
| ASSERT_THAT( | |
| ReadArray(out, | |
| {.path = basePath, .length = length, .io = file_io, .projection = schema}, | |
| &read_metadata), | |
| IsOk()); | |
| ASSERT_EQ(read_metadata, metadata); | |
| ASSERT_TRUE(out != nullptr) << "Reader.Next() returned no data"; | |
| } | |
| struct ParquetCodec { | |
| std::string name; | |
| ::arrow::Compression::type compression; | |
| }; | |
| std::optional<ParquetCodec> FirstUnavailableParquetCodec() { | |
| const std::vector<ParquetCodec> codecs = { | |
| {.name = "snappy", .compression = ::arrow::Compression::SNAPPY}, | |
| {.name = "gzip", .compression = ::arrow::Compression::GZIP}, | |
| {.name = "brotli", .compression = ::arrow::Compression::BROTLI}, | |
| {.name = "lz4", .compression = ::arrow::Compression::LZ4}, | |
| {.name = "zstd", .compression = ::arrow::Compression::ZSTD}, | |
| }; | |
| for (const auto& codec : codecs) { | |
| if (!::arrow::util::Codec::IsAvailable(codec.compression)) { | |
| return codec; | |
| } | |
| } | |
| return std::nullopt; | |
| } | |
| std::unique_ptr<::parquet::FileMetaData> MakeMetadataWithPageOffsets( | |
| const std::vector<std::pair<int64_t, std::optional<int64_t>>>& page_offsets) { | |
| ::parquet::schema::NodeVector fields; | |
| for (size_t i = 0; i < page_offsets.size(); ++i) { | |
| fields.push_back(::parquet::schema::PrimitiveNode::Make( | |
| "col" + std::to_string(i), ::parquet::Repetition::REQUIRED, nullptr, | |
| ::parquet::Type::INT32, /*primitive_length=*/-1, /*field_id=*/i + 1)); | |
| } | |
| ::parquet::SchemaDescriptor schema; | |
| schema.Init(::parquet::schema::GroupNode::Make( | |
| "schema", ::parquet::Repetition::REQUIRED, fields)); | |
| auto builder = ::parquet::FileMetaDataBuilder::Make( | |
| &schema, ::parquet::default_writer_properties()); | |
| auto row_group = builder->AppendRowGroup(); | |
| row_group->set_num_rows(1); | |
| for (const auto& [data_offset, dictionary_offset] : page_offsets) { | |
| row_group->NextColumnChunk()->Finish( | |
| /*num_values=*/1, dictionary_offset.value_or(0), /*index_page_offset=*/0, | |
| data_offset, /*compressed_size=*/10, /*uncompressed_size=*/10, | |
| dictionary_offset.has_value(), /*dictionary_fallback=*/false, {}, {}); | |
| } | |
| row_group->Finish(10 * page_offsets.size()); | |
| return builder->Finish(); | |
| } | |
| constexpr std::array<uint8_t, Uuid::kLength> kUuidBytes1 = { | |
| 0x12, 0x3e, 0x45, 0x67, 0xe8, 0x9b, 0x12, 0xd3, | |
| 0xa4, 0x56, 0x42, 0x66, 0x14, 0x17, 0x40, 0x00}; | |
| constexpr std::array<uint8_t, Uuid::kLength> kUuidBytes2 = { | |
| 0xf7, 0x9c, 0x3e, 0x09, 0x67, 0x7c, 0x4b, 0xbd, | |
| 0xa4, 0x79, 0x3f, 0x34, 0x9c, 0xb7, 0x85, 0xe7}; | |
| } // namespace | |
| TEST(ParquetSplitTest, CanonicalStartUsesFirstColumnPages) { | |
| for (auto dictionary_offset : | |
| {std::optional<int64_t>{}, std::optional<int64_t>{0}, std::optional<int64_t>{80}, | |
| std::optional<int64_t>{100}, std::optional<int64_t>{120}}) { | |
| // The second column starts earlier and must not be used as the assignment key. | |
| auto metadata = MakeMetadataWithPageOffsets({{100, dictionary_offset}, {50, 40}}); | |
| const int64_t expected = dictionary_offset == 80 ? 80 : 100; | |
| EXPECT_THAT(RowGroupStart(*metadata->RowGroup(0), 200), | |
| HasValue(::testing::Eq(expected))); | |
| } | |
| } | |
| TEST(ParquetSplitTest, CanonicalStartRejectsMissingOrOutOfFilePages) { | |
| for (int64_t offset : {-1, 0, 200, 201}) { | |
| auto metadata = MakeMetadataWithPageOffsets({{offset, std::nullopt}}); | |
| EXPECT_THAT(RowGroupStart(*metadata->RowGroup(0), 200), IsError(ErrorKind::kInvalid)); | |
| } | |
| auto no_columns = MakeMetadataWithPageOffsets({}); | |
| EXPECT_THAT(RowGroupStart(*no_columns->RowGroup(0), 200), | |
| HasErrorMessage("without columns")); | |
| } | |
| TEST(ParquetSplitTest, MidpointAddsHalfTheRowGroupSize) { | |
| // Each column chunk is 10 bytes, so the row group is 20 bytes from its start at 80. | |
| auto metadata = MakeMetadataWithPageOffsets({{100, 80}, {50, 40}}); | |
| ASSERT_EQ(metadata->RowGroup(0)->total_compressed_size(), 20); | |
| EXPECT_THAT(RowGroupMidpoint(*metadata->RowGroup(0), 200), HasValue(::testing::Eq(90))); | |
| // The size may reach, but not pass, the end of the file. | |
| EXPECT_THAT(RowGroupMidpoint(*metadata->RowGroup(0), 100), HasValue(::testing::Eq(90))); | |
| EXPECT_THAT(RowGroupMidpoint(*metadata->RowGroup(0), 99), | |
| HasErrorMessage("invalid compressed size 20")); | |
| } | |
| TEST(ParquetSplitTest, MidpointRejectsInvalidStart) { | |
| auto metadata = MakeMetadataWithPageOffsets({{0, std::nullopt}}); | |
| EXPECT_THAT(RowGroupMidpoint(*metadata->RowGroup(0), 200), | |
| HasErrorMessage("starting page 0")); | |
| } | |
| class ParquetReaderTest : public TempFileTestBase { | |
| protected: | |
| static void SetUpTestSuite() { parquet::RegisterAll(); } | |
| void SetUp() override { | |
| TempFileTestBase::SetUp(); | |
| file_io_ = arrow::ArrowFileSystemFileIO::MakeMockFileIO(); | |
| temp_parquet_file_ = "parquet_reader_test.parquet"; | |
| } | |
| void CreateSimpleParquetFile() { | |
| auto schema = std::make_shared<Schema>( | |
| std::vector<SchemaField>{SchemaField::MakeRequired(1, "id", int32()), | |
| SchemaField::MakeOptional(2, "name", string())}); | |
| ArrowSchema arrow_c_schema; | |
| ASSERT_THAT(ToArrowSchema(*schema, &arrow_c_schema), IsOk()); | |
| auto arrow_schema = ::arrow::ImportType(&arrow_c_schema).ValueOrDie(); | |
| auto array = | |
| ::arrow::json::ArrayFromJSONString(::arrow::struct_(arrow_schema->fields()), | |
| R"([[1, "Foo"],[2, "Bar"],[3, "Baz"]])") | |
| .ValueOrDie(); | |
| WriterProperties writer_properties; | |
| writer_properties.Set(WriterProperties::kParquetCompression, | |
| std::string("uncompressed")); | |
| ASSERT_TRUE(WriteArray(array, {.path = temp_parquet_file_, | |
| .schema = schema, | |
| .io = file_io_, | |
| .properties = std::move(writer_properties)})); | |
| } | |
| void CreateParquetFileWithoutFieldIds(const std::shared_ptr<::arrow::DataType>& type, | |
| std::string_view json) { | |
| auto array = ::arrow::json::ArrayFromJSONString(type, json).ValueOrDie(); | |
| auto batch = ::arrow::RecordBatch::FromStructArray(array).ValueOrDie(); | |
| auto table = ::arrow::Table::FromRecordBatches({batch}).ValueOrDie(); | |
| auto io = internal::checked_cast<arrow::ArrowFileSystemFileIO&>(*file_io_); | |
| auto outfile = io.fs()->OpenOutputStream(temp_parquet_file_).ValueOrDie(); | |
| ASSERT_TRUE(::parquet::arrow::WriteTable(*table, ::arrow::default_memory_pool(), | |
| outfile, table->num_rows()) | |
| .ok()); | |
| ASSERT_TRUE(outfile->Close().ok()); | |
| } | |
| void CreateListParquetFile() { | |
| auto schema = std::make_shared<Schema>(std::vector<SchemaField>{ | |
| SchemaField::MakeRequired(1, "id", int32()), | |
| SchemaField::MakeOptional(2, "numbers", | |
| std::make_shared<ListType>(SchemaField::MakeOptional( | |
| /*field_id=*/101, "element", int32())))}); | |
| ArrowSchema arrow_c_schema; | |
| ASSERT_THAT(ToArrowSchema(*schema, &arrow_c_schema), IsOk()); | |
| auto arrow_schema = ::arrow::ImportType(&arrow_c_schema).ValueOrDie(); | |
| auto array = | |
| ::arrow::json::ArrayFromJSONString(::arrow::struct_(arrow_schema->fields()), | |
| R"([[1, [1, 2]], [2, [3]], [3, null]])") | |
| .ValueOrDie(); | |
| WriterProperties writer_properties; | |
| writer_properties.Set(WriterProperties::kParquetCompression, | |
| std::string("uncompressed")); | |
| ASSERT_TRUE(WriteArray(array, {.path = temp_parquet_file_, | |
| .schema = schema, | |
| .io = file_io_, | |
| .properties = std::move(writer_properties)})); | |
| } | |
| void CreateRowLineageParquetFile() { | |
| auto schema = RowLineageSchema(); | |
| ArrowSchema arrow_c_schema; | |
| ASSERT_THAT(ToArrowSchema(*schema, &arrow_c_schema), IsOk()); | |
| auto arrow_schema = ::arrow::ImportType(&arrow_c_schema).ValueOrDie(); | |
| auto array = | |
| ::arrow::json::ArrayFromJSONString(::arrow::struct_(arrow_schema->fields()), | |
| R"([[1, null, null], | |
| [2, 777, 8], | |
| [3, null, null]])") | |
| .ValueOrDie(); | |
| WriterProperties writer_properties; | |
| writer_properties.Set(WriterProperties::kParquetCompression, | |
| std::string("uncompressed")); | |
| ASSERT_TRUE(WriteArray(array, {.path = temp_parquet_file_, | |
| .schema = schema, | |
| .io = file_io_, | |
| .properties = std::move(writer_properties)})); | |
| } | |
| void CreateSplitParquetFile(int64_t row_group_size = 2, bool dictionary = true) { | |
| const std::string kParquetFieldIdKey = "PARQUET:field_id"; | |
| auto arrow_schema = ::arrow::schema( | |
| {::arrow::field("id", ::arrow::int32(), /*nullable=*/false, | |
| ::arrow::KeyValueMetadata::Make({kParquetFieldIdKey}, {"1"})), | |
| ::arrow::field("name", ::arrow::utf8(), /*nullable=*/true, | |
| ::arrow::KeyValueMetadata::Make({kParquetFieldIdKey}, {"2"}))}); | |
| auto table = ::arrow::Table::FromRecordBatches( | |
| arrow_schema, {::arrow::RecordBatch::FromStructArray( | |
| ::arrow::json::ArrayFromJSONString( | |
| ::arrow::struct_(arrow_schema->fields()), | |
| R"([[1, "Foo"], [2, "Bar"], [3, "Baz"]])") | |
| .ValueOrDie()) | |
| .ValueOrDie()}) | |
| .ValueOrDie(); | |
| auto io = internal::checked_cast<arrow::ArrowFileSystemFileIO&>(*file_io_); | |
| auto outfile = io.fs()->OpenOutputStream(temp_parquet_file_).ValueOrDie(); | |
| ::parquet::WriterProperties::Builder properties; | |
| if (!dictionary) { | |
| properties.disable_dictionary(); | |
| } | |
| ASSERT_TRUE(::parquet::arrow::WriteTable(*table, ::arrow::default_memory_pool(), | |
| outfile, row_group_size, properties.build()) | |
| .ok()); | |
| ASSERT_TRUE(outfile->Close().ok()); | |
| } | |
| void ReorderRowGroups(const std::vector<int>& row_groups) { | |
| ICEBERG_UNWRAP_OR_FAIL(auto input, | |
| arrow::OpenArrowInputStream(file_io_, temp_parquet_file_)); | |
| auto metadata = ::parquet::ReadMetaData(input); | |
| auto file_size = input->GetSize().ValueOrDie(); | |
| auto pages = input->ReadAt(0, file_size - metadata->size() - 8).ValueOrDie(); | |
| ASSERT_TRUE(input->Close().ok()); | |
| ICEBERG_UNWRAP_OR_FAIL(auto output, | |
| arrow::OpenArrowOutputStream(file_io_, temp_parquet_file_)); | |
| ASSERT_TRUE(output->Write(pages).ok()); | |
| ::parquet::WriteFileMetaData(*metadata->Subset(row_groups), output.get()); | |
| ASSERT_TRUE(output->Close().ok()); | |
| } | |
| std::shared_ptr<Schema> RowLineageSchema() { | |
| return std::make_shared<Schema>(std::vector<SchemaField>{ | |
| SchemaField::MakeRequired(1, "id", int32()), | |
| MetadataColumns::kRowId, | |
| MetadataColumns::kLastUpdatedSequenceNumber, | |
| }); | |
| } | |
| Result<std::unique_ptr<Reader>> OpenRowLineageReader( | |
| const std::shared_ptr<Schema>& schema, | |
| std::optional<int64_t> first_row_id = std::nullopt, | |
| std::optional<int64_t> data_sequence_number = std::nullopt) { | |
| return ReaderFactoryRegistry::Open(FileFormatType::kParquet, | |
| {.path = temp_parquet_file_, | |
| .io = file_io_, | |
| .projection = schema, | |
| .first_row_id = first_row_id, | |
| .data_sequence_number = data_sequence_number}); | |
| } | |
| // Writes a list parquet file through parquet::arrow::WriteTable, which serializes the | |
| // Arrow schema of the table into the ARROW:schema key value metadata of the file. | |
| void CreateListParquetFileWithArrowSchema() { | |
| const std::string kParquetFieldIdKey = "PARQUET:field_id"; | |
| auto arrow_schema = ::arrow::schema( | |
| {::arrow::field("id", ::arrow::int32(), /*nullable=*/false, | |
| ::arrow::KeyValueMetadata::Make({kParquetFieldIdKey}, {"1"})), | |
| ::arrow::field( | |
| "numbers", | |
| ::arrow::list(::arrow::field( | |
| "element", ::arrow::int32(), /*nullable=*/true, | |
| ::arrow::KeyValueMetadata::Make({kParquetFieldIdKey}, {"101"}))), | |
| /*nullable=*/true, | |
| ::arrow::KeyValueMetadata::Make({kParquetFieldIdKey}, {"2"}))}); | |
| auto batch = | |
| ::arrow::RecordBatch::FromStructArray( | |
| ::arrow::json::ArrayFromJSONString(::arrow::struct_(arrow_schema->fields()), | |
| R"([[1, [1, 2]], [2, [3]]])") | |
| .ValueOrDie()) | |
| .ValueOrDie(); | |
| auto table = ::arrow::Table::FromRecordBatches(arrow_schema, {batch}).ValueOrDie(); | |
| auto io = internal::checked_cast<arrow::ArrowFileSystemFileIO&>(*file_io_); | |
| auto outfile = io.fs()->OpenOutputStream(temp_parquet_file_).ValueOrDie(); | |
| // Build ArrowWriterProperties to store the Arrow schema in ARROW:schema metadata | |
| auto arrow_writer_props = | |
| ::parquet::ArrowWriterProperties::Builder().store_schema()->build(); | |
| // write a single row group so that one batch holds every row | |
| ASSERT_TRUE(::parquet::arrow::WriteTable( | |
| *table, ::arrow::default_memory_pool(), outfile, table->num_rows(), | |
| ::parquet::default_writer_properties(), arrow_writer_props) | |
| .ok()); | |
| ASSERT_TRUE(outfile->Close().ok()); | |
| // Verify ARROW:schema is stored | |
| auto input_file = io.fs()->OpenInputFile(temp_parquet_file_).ValueOrDie(); | |
| auto metadata = ::parquet::ReadMetaData(input_file); | |
| const auto& kv_metadata = metadata->key_value_metadata(); | |
| ASSERT_TRUE(kv_metadata != nullptr); | |
| ASSERT_TRUE(kv_metadata->FindKey("ARROW:schema") >= 0) | |
| << "ARROW:schema not found in file metadata"; | |
| } | |
| // Writes a mixed list/large_list parquet file with stored ARROW:schema. | |
| void CreateMixedListParquetFileWithArrowSchema() { | |
| const std::string kParquetFieldIdKey = "PARQUET:field_id"; | |
| auto arrow_schema = ::arrow::schema( | |
| {::arrow::field("id", ::arrow::int32(), /*nullable=*/false, | |
| ::arrow::KeyValueMetadata::Make({kParquetFieldIdKey}, {"1"})), | |
| ::arrow::field( | |
| "small_lists", | |
| ::arrow::list(::arrow::field( | |
| "element", ::arrow::int32(), /*nullable=*/true, | |
| ::arrow::KeyValueMetadata::Make({kParquetFieldIdKey}, {"101"}))), | |
| /*nullable=*/true, | |
| ::arrow::KeyValueMetadata::Make({kParquetFieldIdKey}, {"2"})), | |
| ::arrow::field( | |
| "large_lists", | |
| ::arrow::large_list(::arrow::field( | |
| "element", ::arrow::int32(), /*nullable=*/true, | |
| ::arrow::KeyValueMetadata::Make({kParquetFieldIdKey}, {"102"}))), | |
| /*nullable=*/true, | |
| ::arrow::KeyValueMetadata::Make({kParquetFieldIdKey}, {"3"}))}); | |
| auto batch = ::arrow::RecordBatch::FromStructArray( | |
| ::arrow::json::ArrayFromJSONString( | |
| ::arrow::struct_(arrow_schema->fields()), | |
| R"([[1, [10, 20], [100, 200]], [2, [30], [300]]])") | |
| .ValueOrDie()) | |
| .ValueOrDie(); | |
| auto table = ::arrow::Table::FromRecordBatches(arrow_schema, {batch}).ValueOrDie(); | |
| auto io = internal::checked_cast<arrow::ArrowFileSystemFileIO&>(*file_io_); | |
| auto outfile = io.fs()->OpenOutputStream(temp_parquet_file_).ValueOrDie(); | |
| // Build ArrowWriterProperties to store the Arrow schema in ARROW:schema metadata | |
| auto arrow_writer_props = | |
| ::parquet::ArrowWriterProperties::Builder().store_schema()->build(); | |
| // write a single row group so that one batch holds every row | |
| ASSERT_TRUE(::parquet::arrow::WriteTable( | |
| *table, ::arrow::default_memory_pool(), outfile, table->num_rows(), | |
| ::parquet::default_writer_properties(), arrow_writer_props) | |
| .ok()); | |
| ASSERT_TRUE(outfile->Close().ok()); | |
| // Verify ARROW:schema is stored | |
| auto input_file = io.fs()->OpenInputFile(temp_parquet_file_).ValueOrDie(); | |
| auto metadata = ::parquet::ReadMetaData(input_file); | |
| const auto& kv_metadata = metadata->key_value_metadata(); | |
| ASSERT_TRUE(kv_metadata != nullptr); | |
| ASSERT_TRUE(kv_metadata->FindKey("ARROW:schema") >= 0) | |
| << "ARROW:schema not found in file metadata"; | |
| } | |
| void VerifyNextBatch(Reader& reader, std::string_view expected_json) { | |
| // Boilerplate to get Arrow schema | |
| auto schema_result = reader.Schema(); | |
| ASSERT_THAT(schema_result, IsOk()); | |
| auto arrow_c_schema = std::move(schema_result.value()); | |
| auto import_schema_result = ::arrow::ImportType(&arrow_c_schema); | |
| auto arrow_schema = import_schema_result.ValueOrDie(); | |
| // Boilerplate to get Arrow array | |
| auto data = reader.Next(); | |
| ASSERT_THAT(data, IsOk()) << "Reader.Next() failed: " << data.error().message; | |
| ASSERT_TRUE(data.value().has_value()) << "Reader.Next() returned no data"; | |
| auto arrow_c_array = data.value().value(); | |
| auto data_result = ::arrow::ImportArray(&arrow_c_array, arrow_schema); | |
| auto arrow_array = data_result.ValueOrDie(); | |
| // Verify data | |
| auto expected_array = | |
| ::arrow::json::ArrayFromJSONString(arrow_schema, expected_json).ValueOrDie(); | |
| ASSERT_TRUE(arrow_array->Equals(*expected_array)); | |
| } | |
| void VerifyExhausted(Reader& reader) { | |
| auto data = reader.Next(); | |
| ASSERT_THAT(data, IsOk()); | |
| ASSERT_FALSE(data.value().has_value()); | |
| } | |
| void VerifyEmptySplitSchema(const std::shared_ptr<Schema>& projection, | |
| const ReaderProperties& properties, | |
| const std::shared_ptr<::arrow::DataType>& expected_type) { | |
| ReaderOptions options{.path = temp_parquet_file_, | |
| .io = file_io_, | |
| .projection = projection, | |
| .properties = properties}; | |
| ICEBERG_UNWRAP_OR_FAIL( | |
| auto full_reader, ReaderFactoryRegistry::Open(FileFormatType::kParquet, options)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto full_c_schema, full_reader->Schema()); | |
| auto full_schema = ::arrow::ImportSchema(&full_c_schema).ValueOrDie(); | |
| ASSERT_THAT(full_reader->Close(), IsOk()); | |
| auto& io = internal::checked_cast<arrow::ArrowFileSystemFileIO&>(*file_io_); | |
| auto file_info = io.fs()->GetFileInfo(temp_parquet_file_).ValueOrDie(); | |
| options.split = Split{.offset = static_cast<size_t>(file_info.size()), .length = 1}; | |
| ICEBERG_UNWRAP_OR_FAIL( | |
| auto reader, ReaderFactoryRegistry::Open(FileFormatType::kParquet, options)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto arrow_c_schema, reader->Schema()); | |
| auto schema = ::arrow::ImportSchema(&arrow_c_schema).ValueOrDie(); | |
| EXPECT_TRUE(schema->Equals(*full_schema, /*check_metadata=*/true)) | |
| << schema->ToString() << " vs " << full_schema->ToString(); | |
| auto actual_type = ::arrow::struct_(schema->fields()); | |
| EXPECT_TRUE(actual_type->Equals(expected_type)) | |
| << actual_type->ToString() << " vs " << expected_type->ToString(); | |
| ASSERT_NO_FATAL_FAILURE(VerifyExhausted(*reader)); | |
| ASSERT_THAT(reader->Close(), IsOk()); | |
| } | |
| std::shared_ptr<FileIO> file_io_; | |
| std::string temp_parquet_file_; | |
| }; | |
| TEST_F(ParquetReaderTest, ReadTwoFields) { | |
| CreateSimpleParquetFile(); | |
| auto schema = std::make_shared<Schema>( | |
| std::vector<SchemaField>{SchemaField::MakeRequired(1, "id", int32()), | |
| SchemaField::MakeOptional(2, "name", string())}); | |
| auto reader_result = ReaderFactoryRegistry::Open( | |
| FileFormatType::kParquet, | |
| {.path = temp_parquet_file_, .io = file_io_, .projection = schema}); | |
| ASSERT_THAT(reader_result, IsOk()) | |
| << "Failed to create reader: " << reader_result.error().message; | |
| auto reader = std::move(reader_result.value()); | |
| ASSERT_NO_FATAL_FAILURE( | |
| VerifyNextBatch(*reader, R"([[1, "Foo"], [2, "Bar"], [3, "Baz"]])")); | |
| ASSERT_NO_FATAL_FAILURE(VerifyExhausted(*reader)); | |
| } | |
| TEST_F(ParquetReaderTest, ReadWithoutFieldIdsOrNameMapping) { | |
| ASSERT_NO_FATAL_FAILURE(CreateParquetFileWithoutFieldIds( | |
| ::arrow::struct_({::arrow::field("id", ::arrow::int32())}), R"([[1]])")); | |
| auto schema = std::make_shared<Schema>( | |
| std::vector<SchemaField>{SchemaField::MakeOptional(1, "id", int32())}); | |
| auto reader_result = ReaderFactoryRegistry::Open( | |
| FileFormatType::kParquet, | |
| {.path = temp_parquet_file_, .io = file_io_, .projection = schema}); | |
| ASSERT_THAT(reader_result, IsError(ErrorKind::kInvalidSchema)); | |
| EXPECT_THAT(reader_result, HasErrorMessage("no field IDs and no name mapping")); | |
| } | |
| TEST_F(ParquetReaderTest, ReadWithNameMapping) { | |
| ASSERT_NO_FATAL_FAILURE(CreateParquetFileWithoutFieldIds( | |
| ::arrow::struct_({::arrow::field("id", ::arrow::int32()), | |
| ::arrow::field("name", ::arrow::utf8())}), | |
| R"([[1, "Foo"], [2, "Bar"], [3, "Baz"]])")); | |
| auto schema = std::make_shared<Schema>(std::vector<SchemaField>{ | |
| SchemaField::MakeOptional(2, "full_name", string()), | |
| SchemaField::MakeOptional(1, "id", int64()), | |
| SchemaField::MakeOptional(3, "extra", string()), | |
| }); | |
| auto mapping = NameMapping::Make({ | |
| MappedField{.names = {"id"}, .field_id = 1}, | |
| MappedField{.names = {"name", "full_name"}, .field_id = 2}, | |
| }); | |
| ICEBERG_UNWRAP_OR_FAIL( | |
| auto reader, ReaderFactoryRegistry::Open(FileFormatType::kParquet, | |
| {.path = temp_parquet_file_, | |
| .io = file_io_, | |
| .projection = schema, | |
| .name_mapping = std::move(mapping)})); | |
| ASSERT_NO_FATAL_FAILURE(VerifyNextBatch( | |
| *reader, R"([["Foo", 1, null], ["Bar", 2, null], ["Baz", 3, null]])")); | |
| ASSERT_NO_FATAL_FAILURE(VerifyExhausted(*reader)); | |
| } | |
| TEST_F(ParquetReaderTest, ReadWithFieldIdsAndNameMapping) { | |
| CreateSimpleParquetFile(); | |
| auto schema = std::make_shared<Schema>(std::vector<SchemaField>{ | |
| SchemaField::MakeRequired(1, "id", int32()), | |
| SchemaField::MakeOptional(2, "name", string()), | |
| }); | |
| auto mapping = NameMapping::Make({ | |
| MappedField{.names = {"id"}, .field_id = 2}, | |
| MappedField{.names = {"name"}, .field_id = 1}, | |
| }); | |
| ICEBERG_UNWRAP_OR_FAIL( | |
| auto reader, ReaderFactoryRegistry::Open(FileFormatType::kParquet, | |
| {.path = temp_parquet_file_, | |
| .io = file_io_, | |
| .projection = schema, | |
| .name_mapping = std::move(mapping)})); | |
| ASSERT_NO_FATAL_FAILURE( | |
| VerifyNextBatch(*reader, R"([[1, "Foo"], [2, "Bar"], [3, "Baz"]])")); | |
| ASSERT_NO_FATAL_FAILURE(VerifyExhausted(*reader)); | |
| } | |
| TEST_F(ParquetReaderTest, ReadNestedFieldsWithNameMapping) { | |
| auto value_type = ::arrow::struct_({::arrow::field("value", ::arrow::int32())}); | |
| auto type = ::arrow::struct_({ | |
| ::arrow::field("profile", value_type), | |
| ::arrow::field("numbers", ::arrow::list(::arrow::field("item", value_type))), | |
| ::arrow::field("counts", ::arrow::map(::arrow::utf8(), value_type)), | |
| }); | |
| const std::string json = R"([ | |
| [[1], [[2], null], [["a", [3]], ["b", null]]], | |
| [null, [], []], | |
| [[null], null, null] | |
| ])"; | |
| ASSERT_NO_FATAL_FAILURE(CreateParquetFileWithoutFieldIds(type, json)); | |
| auto schema = std::make_shared<Schema>(std::vector<SchemaField>{ | |
| SchemaField::MakeOptional(1, "profile", | |
| std::make_shared<StructType>(std::vector<SchemaField>{ | |
| SchemaField::MakeOptional(101, "value", int64()), | |
| })), | |
| SchemaField::MakeOptional(2, "numbers", | |
| std::make_shared<ListType>(SchemaField::MakeOptional( | |
| 201, "element", | |
| std::make_shared<StructType>(std::vector<SchemaField>{ | |
| SchemaField::MakeOptional(202, "value", int64()), | |
| })))), | |
| SchemaField::MakeOptional( | |
| 3, "counts", | |
| std::make_shared<MapType>( | |
| SchemaField::MakeRequired(301, "key", string()), | |
| SchemaField::MakeOptional( | |
| 302, "value", | |
| std::make_shared<StructType>(std::vector<SchemaField>{ | |
| SchemaField::MakeOptional(303, "value", int64()), | |
| })))), | |
| }); | |
| ICEBERG_UNWRAP_OR_FAIL(auto unique_mapping, CreateMapping(*schema)); | |
| std::shared_ptr<NameMapping> mapping = std::move(unique_mapping); | |
| for (bool use_large_list : {false, true}) { | |
| SCOPED_TRACE(use_large_list); | |
| ReaderProperties properties; | |
| properties.Set(ReaderProperties::kArrowUseLargeList, use_large_list); | |
| ICEBERG_UNWRAP_OR_FAIL( | |
| auto reader, ReaderFactoryRegistry::Open(FileFormatType::kParquet, | |
| {.path = temp_parquet_file_, | |
| .io = file_io_, | |
| .projection = schema, | |
| .name_mapping = mapping, | |
| .properties = std::move(properties)})); | |
| ASSERT_NO_FATAL_FAILURE(VerifyNextBatch(*reader, json)); | |
| ASSERT_NO_FATAL_FAILURE(VerifyExhausted(*reader)); | |
| } | |
| } | |
| TEST_F(ParquetReaderTest, ReadMissingFieldsWithDefaults) { | |
| // The file contains only fields 1 and 2; the projected schema adds fields 3 and 4 | |
| // with initial-defaults, which are filled for all rows written before the columns | |
| // existed. | |
| CreateSimpleParquetFile(); | |
| auto schema = std::make_shared<Schema>(std::vector<SchemaField>{ | |
| SchemaField::MakeRequired(1, "id", int32()), | |
| SchemaField::MakeOptional(2, "name", string()), | |
| SchemaField(3, "score", int64(), /*optional=*/false, /*doc=*/{}, | |
| std::make_shared<const Literal>(Literal::Long(100))), | |
| SchemaField(4, "status", string(), /*optional=*/true, /*doc=*/{}, | |
| std::make_shared<const Literal>(Literal::String("active"))), | |
| }); | |
| auto reader_result = ReaderFactoryRegistry::Open( | |
| FileFormatType::kParquet, | |
| {.path = temp_parquet_file_, .io = file_io_, .projection = schema}); | |
| ASSERT_THAT(reader_result, IsOk()) | |
| << "Failed to create reader: " << reader_result.error().message; | |
| auto reader = std::move(reader_result.value()); | |
| ASSERT_NO_FATAL_FAILURE(VerifyNextBatch(*reader, | |
| R"([[1, "Foo", 100, "active"], | |
| [2, "Bar", 100, "active"], | |
| [3, "Baz", 100, "active"]])")); | |
| ASSERT_NO_FATAL_FAILURE(VerifyExhausted(*reader)); | |
| } | |
| TEST_F(ParquetReaderTest, ReadOnlyDefaultColumn) { | |
| // The projected schema selects a single column that is absent from the file, so no | |
| // physical column is read. The default must still be filled once per row, using the | |
| // file's row count as the anchor. | |
| CreateSimpleParquetFile(); | |
| auto schema = std::make_shared<Schema>(std::vector<SchemaField>{ | |
| SchemaField(3, "score", int64(), /*optional=*/false, /*doc=*/{}, | |
| std::make_shared<const Literal>(Literal::Long(100))), | |
| }); | |
| auto reader_result = ReaderFactoryRegistry::Open( | |
| FileFormatType::kParquet, | |
| {.path = temp_parquet_file_, .io = file_io_, .projection = schema}); | |
| ASSERT_THAT(reader_result, IsOk()) | |
| << "Failed to create reader: " << reader_result.error().message; | |
| auto reader = std::move(reader_result.value()); | |
| ASSERT_NO_FATAL_FAILURE(VerifyNextBatch(*reader, R"([[100], [100], [100]])")); | |
| ASSERT_NO_FATAL_FAILURE(VerifyExhausted(*reader)); | |
| } | |
| TEST_F(ParquetReaderTest, RoundTripWithGenericFileIO) { | |
| auto file_io = std::make_shared<iceberg::test::StdFileIO>(); | |
| auto path = CreateNewTempFilePathWithSuffix(".parquet"); | |
| auto schema = std::make_shared<Schema>( | |
| std::vector<SchemaField>{SchemaField::MakeRequired(1, "id", int32()), | |
| SchemaField::MakeOptional(2, "name", string())}); | |
| ArrowSchema arrow_c_schema; | |
| ASSERT_THAT(ToArrowSchema(*schema, &arrow_c_schema), IsOk()); | |
| auto arrow_schema = ::arrow::ImportType(&arrow_c_schema).ValueOrDie(); | |
| auto array = | |
| ::arrow::json::ArrayFromJSONString(::arrow::struct_(arrow_schema->fields()), | |
| R"([[1, "Foo"], [2, "Bar"]])") | |
| .ValueOrDie(); | |
| WriterProperties writer_properties; | |
| writer_properties.Set(WriterProperties::kParquetCompression, | |
| std::string("uncompressed")); | |
| auto writer_result = WriterFactoryRegistry::Open( | |
| FileFormatType::kParquet, {.path = path, | |
| .schema = schema, | |
| .io = file_io, | |
| .properties = std::move(writer_properties)}); | |
| ASSERT_THAT(writer_result, IsOk()); | |
| auto writer = std::move(writer_result.value()); | |
| ASSERT_THAT(WriteArray(array, *writer), IsOk()); | |
| ICEBERG_UNWRAP_OR_FAIL(auto length, writer->length()); | |
| std::shared_ptr<::arrow::Array> out; | |
| auto read_status = ReadArray( | |
| out, {.path = path, .length = length, .io = file_io, .projection = schema}, | |
| nullptr); | |
| ASSERT_THAT(read_status, IsOk()); | |
| ASSERT_TRUE(out->Equals(*array)); | |
| } | |
| TEST_F(ParquetReaderTest, ReadSmallDecimalsWithStoredArrowSchema) { | |
| auto schema = std::make_shared<Schema>(std::vector<SchemaField>{ | |
| SchemaField::MakeRequired(1, "value", decimal(20, 2)), | |
| }); | |
| for (const auto& type : {::arrow::decimal32(9, 2), ::arrow::decimal64(18, 2)}) { | |
| SCOPED_TRACE(type->ToString()); | |
| auto arrow_schema = ::arrow::schema( | |
| {::arrow::field("value", type, /*nullable=*/false, | |
| ::arrow::key_value_metadata({"PARQUET:field_id"}, {"1"}))}); | |
| auto values = ::arrow::json::ArrayFromJSONString(type, R"(["-1.23", "0.00", "4.56"])") | |
| .ValueOrDie(); | |
| auto table = ::arrow::Table::Make(arrow_schema, {values}); | |
| auto& io = internal::checked_cast<arrow::ArrowFileSystemFileIO&>(*file_io_); | |
| auto output = io.fs()->OpenOutputStream(temp_parquet_file_).ValueOrDie(); | |
| ASSERT_TRUE(::parquet::arrow::WriteTable( | |
| *table, ::arrow::default_memory_pool(), output, table->num_rows(), | |
| ::parquet::WriterProperties::Builder() | |
| .enable_store_decimal_as_integer() | |
| ->build(), | |
| ::parquet::ArrowWriterProperties::Builder().store_schema()->build()) | |
| .ok()); | |
| ASSERT_TRUE(output->Close().ok()); | |
| auto reader = ReaderFactoryRegistry::Open( | |
| FileFormatType::kParquet, | |
| {.path = temp_parquet_file_, .io = file_io_, .projection = schema}); | |
| ASSERT_THAT(reader, IsOk()); | |
| ASSERT_NO_FATAL_FAILURE( | |
| VerifyNextBatch(**reader, R"([["-1.23"], ["0.00"], ["4.56"]])")); | |
| ASSERT_NO_FATAL_FAILURE(VerifyExhausted(**reader)); | |
| } | |
| } | |
| TEST_F(ParquetReaderTest, ReadReorderedFieldsWithNulls) { | |
| CreateSimpleParquetFile(); | |
| auto schema = std::make_shared<Schema>( | |
| std::vector<SchemaField>{SchemaField::MakeOptional(2, "name", string()), | |
| SchemaField::MakeRequired(1, "id", int32()), | |
| SchemaField::MakeOptional(3, "score", float64())}); | |
| auto reader_result = ReaderFactoryRegistry::Open( | |
| FileFormatType::kParquet, | |
| {.path = temp_parquet_file_, .io = file_io_, .projection = schema}); | |
| ASSERT_THAT(reader_result, IsOk()); | |
| auto reader = std::move(reader_result.value()); | |
| ASSERT_NO_FATAL_FAILURE(VerifyNextBatch( | |
| *reader, R"([["Foo", 1, null], ["Bar", 2, null], ["Baz", 3, null]])")); | |
| ASSERT_NO_FATAL_FAILURE(VerifyExhausted(*reader)); | |
| } | |
| TEST_F(ParquetReaderTest, ReadWithBatchSize) { | |
| CreateSimpleParquetFile(); | |
| auto schema = std::make_shared<Schema>( | |
| std::vector<SchemaField>{SchemaField::MakeRequired(1, "id", int32())}); | |
| ReaderProperties reader_properties; | |
| reader_properties.Set(ReaderProperties::kBatchSize, int64_t{2}); | |
| auto reader_result = ReaderFactoryRegistry::Open( | |
| FileFormatType::kParquet, {.path = temp_parquet_file_, | |
| .io = file_io_, | |
| .projection = schema, | |
| .properties = std::move(reader_properties)}); | |
| ASSERT_THAT(reader_result, IsOk()); | |
| auto reader = std::move(reader_result.value()); | |
| ASSERT_NO_FATAL_FAILURE(VerifyNextBatch(*reader, R"([[1], [2]])")); | |
| ASSERT_NO_FATAL_FAILURE(VerifyNextBatch(*reader, R"([[3]])")); | |
| ASSERT_NO_FATAL_FAILURE(VerifyExhausted(*reader)); | |
| } | |
| TEST_F(ParquetReaderTest, ReadListType) { | |
| CreateListParquetFile(); | |
| auto schema = std::make_shared<Schema>(std::vector<SchemaField>{ | |
| SchemaField::MakeRequired(1, "id", int32()), | |
| SchemaField::MakeOptional(2, "numbers", | |
| std::make_shared<ListType>(SchemaField::MakeOptional( | |
| /*field_id=*/101, "element", int32())))}); | |
| auto reader_result = ReaderFactoryRegistry::Open( | |
| FileFormatType::kParquet, | |
| {.path = temp_parquet_file_, .io = file_io_, .projection = schema}); | |
| ASSERT_THAT(reader_result, IsOk()); | |
| auto reader = std::move(reader_result.value()); | |
| // By default list columns are read as 32-bit offset list arrays. | |
| auto schema_result = reader->Schema(); | |
| ASSERT_THAT(schema_result, IsOk()); | |
| auto arrow_c_schema = std::move(schema_result.value()); | |
| auto arrow_type = ::arrow::ImportType(&arrow_c_schema).ValueOrDie(); | |
| ASSERT_EQ(arrow_type->field(1)->type()->id(), ::arrow::Type::LIST); | |
| ASSERT_NO_FATAL_FAILURE( | |
| VerifyNextBatch(*reader, R"([[1, [1, 2]], [2, [3]], [3, null]])")); | |
| ASSERT_NO_FATAL_FAILURE(VerifyExhausted(*reader)); | |
| } | |
| TEST_F(ParquetReaderTest, ReadListAsLargeList) { | |
| CreateListParquetFile(); | |
| auto schema = std::make_shared<Schema>(std::vector<SchemaField>{ | |
| SchemaField::MakeRequired(1, "id", int32()), | |
| SchemaField::MakeOptional(2, "numbers", | |
| std::make_shared<ListType>(SchemaField::MakeOptional( | |
| /*field_id=*/101, "element", int32())))}); | |
| ReaderProperties reader_properties; | |
| reader_properties.Set(ReaderProperties::kArrowUseLargeList, true); | |
| auto reader_result = ReaderFactoryRegistry::Open( | |
| FileFormatType::kParquet, {.path = temp_parquet_file_, | |
| .io = file_io_, | |
| .projection = schema, | |
| .properties = std::move(reader_properties)}); | |
| ASSERT_THAT(reader_result, IsOk()); | |
| auto reader = std::move(reader_result.value()); | |
| // The output schema should expose list columns as large_list. | |
| auto schema_result = reader->Schema(); | |
| ASSERT_THAT(schema_result, IsOk()); | |
| auto arrow_c_schema = std::move(schema_result.value()); | |
| auto arrow_type = ::arrow::ImportType(&arrow_c_schema).ValueOrDie(); | |
| ASSERT_EQ(arrow_type->field(1)->type()->id(), ::arrow::Type::LARGE_LIST); | |
| // JSON parsing creates regular ListArray, so verify large_list data manually. | |
| auto data = reader->Next(); | |
| ASSERT_THAT(data, IsOk()); | |
| ASSERT_TRUE(data.value().has_value()); | |
| auto arrow_c_array = data.value().value(); | |
| auto arrow_array = ::arrow::ImportArray(&arrow_c_array, arrow_type).ValueOrDie(); | |
| const auto& struct_array = | |
| internal::checked_cast<const ::arrow::StructArray&>(*arrow_array); | |
| ASSERT_EQ(struct_array.length(), 3); | |
| const auto& id_array = | |
| internal::checked_cast<const ::arrow::Int32Array&>(*struct_array.field(0)); | |
| ASSERT_EQ(id_array.Value(0), 1); | |
| ASSERT_EQ(id_array.Value(1), 2); | |
| ASSERT_EQ(id_array.Value(2), 3); | |
| const auto& numbers_array = | |
| internal::checked_cast<const ::arrow::LargeListArray&>(*struct_array.field(1)); | |
| ASSERT_EQ(numbers_array.value_slice(0)->length(), 2); | |
| ASSERT_EQ(numbers_array.value_slice(1)->length(), 1); | |
| ASSERT_TRUE(numbers_array.IsNull(2)); | |
| ASSERT_NO_FATAL_FAILURE(VerifyExhausted(*reader)); | |
| } | |
| TEST_F(ParquetReaderTest, ReadListAsLargeListWithArrowSchema) { | |
| // Reading a file that carries serialized ARROW:schema metadata must report an output | |
| // schema that describes the arrays the reader actually produces. Arrow decides the list | |
| // type of the arrays, so the output schema follows the schema of the reader instead of | |
| // assuming that the requested large_list type was applied. | |
| CreateListParquetFileWithArrowSchema(); | |
| auto schema = std::make_shared<Schema>(std::vector<SchemaField>{ | |
| SchemaField::MakeRequired(1, "id", int32()), | |
| SchemaField::MakeOptional(2, "numbers", | |
| std::make_shared<ListType>(SchemaField::MakeOptional( | |
| /*field_id=*/101, "element", int32())))}); | |
| ReaderProperties reader_properties; | |
| reader_properties.Set(ReaderProperties::kArrowUseLargeList, true); | |
| auto reader_result = ReaderFactoryRegistry::Open( | |
| FileFormatType::kParquet, {.path = temp_parquet_file_, | |
| .io = file_io_, | |
| .projection = schema, | |
| .properties = std::move(reader_properties)}); | |
| ASSERT_THAT(reader_result, IsOk()); | |
| auto reader = std::move(reader_result.value()); | |
| auto schema_result = reader->Schema(); | |
| ASSERT_THAT(schema_result, IsOk()); | |
| auto arrow_c_schema = std::move(schema_result.value()); | |
| auto arrow_type = ::arrow::ImportType(&arrow_c_schema).ValueOrDie(); | |
| auto list_type_id = arrow_type->field(1)->type()->id(); | |
| ASSERT_TRUE(list_type_id == ::arrow::Type::LIST || | |
| list_type_id == ::arrow::Type::LARGE_LIST) | |
| << "unexpected list type: " << arrow_type->field(1)->type()->ToString(); | |
| auto data = reader->Next(); | |
| ASSERT_THAT(data, IsOk()); | |
| ASSERT_TRUE(data.value().has_value()); | |
| auto arrow_c_array = data.value().value(); | |
| // Importing the array against the reported schema fails if the two disagree, which is | |
| // what this test guards: the output schema must not claim a list type that the reader | |
| // did not produce. | |
| auto import_result = ::arrow::ImportArray(&arrow_c_array, arrow_type); | |
| ASSERT_TRUE(import_result.ok()) << import_result.status().ToString(); | |
| auto arrow_array = import_result.ValueOrDie(); | |
| ASSERT_TRUE(arrow_array->ValidateFull().ok()); | |
| const auto& struct_array = | |
| internal::checked_cast<const ::arrow::StructArray&>(*arrow_array); | |
| ASSERT_EQ(struct_array.length(), 2); | |
| const auto& id_array = | |
| internal::checked_cast<const ::arrow::Int32Array&>(*struct_array.field(0)); | |
| ASSERT_EQ(id_array.Value(0), 1); | |
| ASSERT_EQ(id_array.Value(1), 2); | |
| // The list offsets are 32 or 64 bit wide depending on the type the reader produced. | |
| ASSERT_EQ(struct_array.field(1)->type()->id(), list_type_id); | |
| if (list_type_id == ::arrow::Type::LARGE_LIST) { | |
| const auto& numbers_array = | |
| internal::checked_cast<const ::arrow::LargeListArray&>(*struct_array.field(1)); | |
| ASSERT_EQ(numbers_array.value_slice(0)->length(), 2); | |
| ASSERT_EQ(numbers_array.value_slice(1)->length(), 1); | |
| } else { | |
| const auto& numbers_array = | |
| internal::checked_cast<const ::arrow::ListArray&>(*struct_array.field(1)); | |
| ASSERT_EQ(numbers_array.value_slice(0)->length(), 2); | |
| ASSERT_EQ(numbers_array.value_slice(1)->length(), 1); | |
| } | |
| } | |
| TEST_F(ParquetReaderTest, ReadMixedListAndLargeListWithArrowSchema) { | |
| // Reading a file with both list and large_list fields (stored via ARROW:schema | |
| // metadata) must report an output schema that describes the actual types of each field, | |
| // preserving the per-field list type. This tests the per-field mapping logic. | |
| CreateMixedListParquetFileWithArrowSchema(); | |
| auto schema = std::make_shared<Schema>(std::vector<SchemaField>{ | |
| SchemaField::MakeRequired(1, "id", int32()), | |
| SchemaField::MakeOptional(2, "small_lists", | |
| std::make_shared<ListType>(SchemaField::MakeOptional( | |
| /*field_id=*/101, "element", int32()))), | |
| SchemaField::MakeOptional(3, "large_lists", | |
| std::make_shared<ListType>(SchemaField::MakeOptional( | |
| /*field_id=*/102, "element", int32())))}); | |
| // Read with default setting (use_large_list=false) | |
| auto reader_result = ReaderFactoryRegistry::Open( | |
| FileFormatType::kParquet, | |
| {.path = temp_parquet_file_, .io = file_io_, .projection = schema}); | |
| ASSERT_THAT(reader_result, IsOk()); | |
| auto reader = std::move(reader_result.value()); | |
| // Verify the output schema has the correct per-field list types | |
| auto schema_result = reader->Schema(); | |
| ASSERT_THAT(schema_result, IsOk()); | |
| auto arrow_c_schema = std::move(schema_result.value()); | |
| auto arrow_type = ::arrow::ImportType(&arrow_c_schema).ValueOrDie(); | |
| // small_lists should be LIST (as stored in the file) | |
| ASSERT_EQ(arrow_type->field(1)->type()->id(), ::arrow::Type::LIST) | |
| << "small_lists field should be LIST"; | |
| // large_lists should be LARGE_LIST (as stored in the file) | |
| ASSERT_EQ(arrow_type->field(2)->type()->id(), ::arrow::Type::LARGE_LIST) | |
| << "large_lists field should be LARGE_LIST"; | |
| // Verify the arrays are readable with the reported schema | |
| auto data = reader->Next(); | |
| ASSERT_THAT(data, IsOk()); | |
| ASSERT_TRUE(data.value().has_value()); | |
| auto arrow_c_array = data.value().value(); | |
| auto import_result = ::arrow::ImportArray(&arrow_c_array, arrow_type); | |
| ASSERT_TRUE(import_result.ok()) << import_result.status().ToString(); | |
| auto arrow_array = import_result.ValueOrDie(); | |
| ASSERT_TRUE(arrow_array->ValidateFull().ok()); | |
| const auto& struct_array = | |
| internal::checked_cast<const ::arrow::StructArray&>(*arrow_array); | |
| ASSERT_EQ(struct_array.length(), 2); | |
| // Verify the field types in the actual arrays | |
| ASSERT_EQ(struct_array.field(1)->type()->id(), ::arrow::Type::LIST); | |
| ASSERT_EQ(struct_array.field(2)->type()->id(), ::arrow::Type::LARGE_LIST); | |
| ASSERT_NO_FATAL_FAILURE(VerifyExhausted(*reader)); | |
| } | |
| TEST_F(ParquetReaderTest, ReadRenamedLargeListColumnWithArrowSchema) { | |
| // The projected column has a different name than the file but the same field id. The | |
| // output schema must be aligned to the reader by field id, not by name: the file stores | |
| // this column as large_list (via ARROW:schema), so a name-based match would miss the | |
| // rename, report the column as list while the array is large_list, and fail to import | |
| // it. | |
| CreateMixedListParquetFileWithArrowSchema(); | |
| // field id 3 is stored in the file as a large_list named "large_lists"; project it | |
| // under a different name to force matching by field id | |
| auto schema = std::make_shared<Schema>(std::vector<SchemaField>{ | |
| SchemaField::MakeOptional(3, "renamed", | |
| std::make_shared<ListType>(SchemaField::MakeOptional( | |
| /*field_id=*/102, "element", int32())))}); | |
| // read with the default use_large_list=false, so only field-id matching can preserve | |
| // large_list | |
| auto reader_result = ReaderFactoryRegistry::Open( | |
| FileFormatType::kParquet, | |
| {.path = temp_parquet_file_, .io = file_io_, .projection = schema}); | |
| ASSERT_THAT(reader_result, IsOk()); | |
| auto reader = std::move(reader_result.value()); | |
| auto schema_result = reader->Schema(); | |
| ASSERT_THAT(schema_result, IsOk()); | |
| auto arrow_c_schema = std::move(schema_result.value()); | |
| auto arrow_type = ::arrow::ImportType(&arrow_c_schema).ValueOrDie(); | |
| // the renamed column keeps the file's large_list type because it is matched by field id | |
| ASSERT_EQ(arrow_type->field(0)->type()->id(), ::arrow::Type::LARGE_LIST) | |
| << "renamed column must keep the file's large_list type, matched by field id"; | |
| // importing the produced array against the reported schema must succeed; a name-based | |
| // mismatch would report list here while the array is large_list and this import would | |
| // fail | |
| auto data = reader->Next(); | |
| ASSERT_THAT(data, IsOk()); | |
| ASSERT_TRUE(data.value().has_value()); | |
| auto arrow_c_array = data.value().value(); | |
| auto import_result = ::arrow::ImportArray(&arrow_c_array, arrow_type); | |
| ASSERT_TRUE(import_result.ok()) << import_result.status().ToString(); | |
| ASSERT_TRUE(import_result.ValueOrDie()->ValidateFull().ok()); | |
| ASSERT_NO_FATAL_FAILURE(VerifyExhausted(*reader)); | |
| } | |
| TEST_F(ParquetReaderTest, EmptySplitReadsListsAsLargeListWhenRequested) { | |
| ASSERT_NO_FATAL_FAILURE(CreateListParquetFile()); | |
| auto schema = | |
| std::make_shared<Schema>(std::vector<SchemaField>{SchemaField::MakeOptional( | |
| 2, "numbers", list(SchemaField::MakeOptional(101, "element", int32())))}); | |
| for (bool use_large_list : {false, true}) { | |
| SCOPED_TRACE(use_large_list); | |
| ReaderProperties properties; | |
| properties.Set(ReaderProperties::kArrowUseLargeList, use_large_list); | |
| auto element = ::arrow::field("element", ::arrow::int32()); | |
| auto list_type = | |
| use_large_list ? ::arrow::large_list(element) : ::arrow::list(element); | |
| auto expected_type = ::arrow::struct_({::arrow::field("numbers", list_type)}); | |
| ASSERT_NO_FATAL_FAILURE(VerifyEmptySplitSchema(schema, properties, expected_type)); | |
| } | |
| } | |
| TEST_F(ParquetReaderTest, EmptySplitKeepsStoredListTypes) { | |
| ASSERT_NO_FATAL_FAILURE(CreateMixedListParquetFileWithArrowSchema()); | |
| auto schema = std::make_shared<Schema>(std::vector<SchemaField>{ | |
| SchemaField::MakeOptional(3, "renamed", | |
| list(SchemaField::MakeOptional(102, "element", int32()))), | |
| SchemaField::MakeOptional( | |
| 2, "small_lists", list(SchemaField::MakeOptional(101, "element", int32())))}); | |
| auto element = ::arrow::field("element", ::arrow::int32()); | |
| auto expected_type = | |
| ::arrow::struct_({::arrow::field("renamed", ::arrow::large_list(element)), | |
| ::arrow::field("small_lists", ::arrow::list(element))}); | |
| for (bool use_large_list : {false, true}) { | |
| SCOPED_TRACE(use_large_list); | |
| ReaderProperties properties; | |
| properties.Set(ReaderProperties::kArrowUseLargeList, use_large_list); | |
| ASSERT_NO_FATAL_FAILURE(VerifyEmptySplitSchema(schema, properties, expected_type)); | |
| } | |
| } | |
| TEST_F(ParquetReaderTest, EmptySplitAppliesLargeListToMissingFields) { | |
| ASSERT_NO_FATAL_FAILURE(CreateSimpleParquetFile()); | |
| for (bool include_physical_field : {false, true}) { | |
| SCOPED_TRACE(include_physical_field); | |
| for (bool use_large_list : {false, true}) { | |
| SCOPED_TRACE(use_large_list); | |
| auto element = ::arrow::field("element", ::arrow::int32()); | |
| auto list_type = | |
| use_large_list ? ::arrow::large_list(element) : ::arrow::list(element); | |
| std::vector<SchemaField> fields{SchemaField::MakeOptional( | |
| 9, "missing", list(SchemaField::MakeOptional(109, "element", int32())))}; | |
| ::arrow::FieldVector expected_fields{::arrow::field("missing", list_type)}; | |
| if (include_physical_field) { | |
| fields.push_back(SchemaField::MakeRequired(1, "id", int32())); | |
| expected_fields.push_back(::arrow::field("id", ::arrow::int32(), false)); | |
| } | |
| auto schema = std::make_shared<Schema>(std::move(fields)); | |
| ReaderProperties properties; | |
| properties.Set(ReaderProperties::kArrowUseLargeList, use_large_list); | |
| ASSERT_NO_FATAL_FAILURE(VerifyEmptySplitSchema( | |
| schema, properties, ::arrow::struct_(std::move(expected_fields)))); | |
| } | |
| } | |
| } | |
| TEST_F(ParquetReaderTest, EmptySplitWithoutPhysicalColumns) { | |
| ASSERT_NO_FATAL_FAILURE(CreateSimpleParquetFile()); | |
| for (const auto& fields : {std::vector<SchemaField>{}, | |
| std::vector<SchemaField>{MetadataColumns::kFilePath, | |
| MetadataColumns::kRowPosition}}) { | |
| SCOPED_TRACE(fields.size()); | |
| auto schema = std::make_shared<Schema>(fields); | |
| ArrowSchema arrow_c_schema; | |
| ASSERT_THAT(ToArrowSchema(*schema, &arrow_c_schema), IsOk()); | |
| auto expected_type = ::arrow::ImportType(&arrow_c_schema).ValueOrDie(); | |
| ASSERT_NO_FATAL_FAILURE(VerifyEmptySplitSchema(schema, {}, expected_type)); | |
| } | |
| } | |
| TEST_F(ParquetReaderTest, EmptySplitKeepsPrunedNestedListTypes) { | |
| auto write_schema = | |
| std::make_shared<Schema>(std::vector<SchemaField>{SchemaField::MakeOptional( | |
| 1, "profile", | |
| struct_({SchemaField::MakeOptional(2, "ignored", int32()), | |
| SchemaField::MakeOptional( | |
| 3, "numbers", | |
| list(SchemaField::MakeOptional(103, "element", int32())))}))}); | |
| ArrowSchema arrow_c_schema; | |
| ASSERT_THAT(ToArrowSchema(*write_schema, &arrow_c_schema), IsOk()); | |
| auto arrow_type = ::arrow::ImportType(&arrow_c_schema).ValueOrDie(); | |
| auto array = ::arrow::json::ArrayFromJSONString( | |
| arrow_type, R"([[{"ignored": 1, "numbers": [1, 2]}]])") | |
| .ValueOrDie(); | |
| WriterProperties writer_properties; | |
| writer_properties.Set(WriterProperties::kParquetCompression, | |
| std::string("uncompressed")); | |
| ASSERT_THAT(WriteArray(array, {.path = temp_parquet_file_, | |
| .schema = write_schema, | |
| .io = file_io_, | |
| .properties = std::move(writer_properties)}), | |
| IsOk()); | |
| auto schema = | |
| std::make_shared<Schema>(std::vector<SchemaField>{SchemaField::MakeOptional( | |
| 1, "profile", | |
| struct_({SchemaField::MakeOptional( | |
| 3, "renamed", | |
| list(SchemaField::MakeOptional(103, "element", int32())))}))}); | |
| for (bool use_large_list : {false, true}) { | |
| SCOPED_TRACE(use_large_list); | |
| ReaderProperties properties; | |
| properties.Set(ReaderProperties::kArrowUseLargeList, use_large_list); | |
| auto element = ::arrow::field("element", ::arrow::int32()); | |
| auto list_type = | |
| use_large_list ? ::arrow::large_list(element) : ::arrow::list(element); | |
| auto expected_type = ::arrow::struct_({::arrow::field( | |
| "profile", ::arrow::struct_({::arrow::field("renamed", list_type)}))}); | |
| ASSERT_NO_FATAL_FAILURE(VerifyEmptySplitSchema(schema, properties, expected_type)); | |
| } | |
| } | |
| TEST_F(ParquetReaderTest, ReadSplit) { | |
| CreateSplitParquetFile(); | |
| // Read row-group starts and midpoints | |
| auto io = internal::checked_cast<arrow::ArrowFileSystemFileIO&>(*file_io_); | |
| auto input_stream = io.fs()->OpenInputFile(temp_parquet_file_).ValueOrDie(); | |
| auto metadata = ::parquet::ReadMetaData(input_stream); | |
| const int64_t file_size = input_stream->GetSize().ValueOrDie(); | |
| std::vector<size_t> starts; | |
| std::vector<size_t> midpoints; | |
| for (int i = 0; i < metadata->num_row_groups(); ++i) { | |
| auto column = metadata->RowGroup(i)->ColumnChunk(0); | |
| ASSERT_TRUE(column->has_dictionary_page()); | |
| ASSERT_LT(column->dictionary_page_offset(), column->data_page_offset()); | |
| starts.push_back(static_cast<size_t>(column->dictionary_page_offset())); | |
| ICEBERG_UNWRAP_OR_FAIL(auto midpoint, | |
| RowGroupMidpoint(*metadata->RowGroup(i), file_size)); | |
| ASSERT_GT(static_cast<size_t>(midpoint), starts.back()); | |
| midpoints.push_back(static_cast<size_t>(midpoint)); | |
| } | |
| ASSERT_EQ(starts.size(), 2); | |
| auto schema = std::make_shared<Schema>(std::vector<SchemaField>{ | |
| SchemaField::MakeRequired(1, "id", int32()), | |
| MetadataColumns::kRowPosition, | |
| }); | |
| std::vector<Split> splits = { | |
| {.offset = 0, .length = std::numeric_limits<size_t>::max()}, | |
| {.offset = starts[0], .length = starts[1] - starts[0]}, | |
| {.offset = midpoints[1], .length = 1}, | |
| {.offset = midpoints[1] + 1, .length = std::numeric_limits<size_t>::max()}, | |
| {.offset = 0, .length = midpoints[0]}, | |
| {.offset = midpoints[0] + 1, .length = midpoints[1] - midpoints[0]}, | |
| {.offset = starts[0], .length = 0}, | |
| // A row group belongs to the split containing its midpoint, not its start. | |
| {.offset = starts[1], .length = 1}, | |
| {.offset = starts[0] + 1, .length = midpoints[0] - starts[0]}, | |
| }; | |
| std::vector<std::string> expected_json = { | |
| R"([[1, 0], [2, 1], [3, 2]])", | |
| R"([[1, 0], [2, 1]])", | |
| R"([[3, 2]])", | |
| "", | |
| "", | |
| R"([[3, 2]])", | |
| "", | |
| "", | |
| R"([[1, 0], [2, 1]])", | |
| }; | |
| ReaderProperties reader_properties; | |
| reader_properties.Set(ReaderProperties::kBatchSize, int64_t{100}); | |
| for (size_t i = 0; i < splits.size(); ++i) { | |
| auto reader_result = ReaderFactoryRegistry::Open(FileFormatType::kParquet, | |
| { | |
| .path = temp_parquet_file_, | |
| .split = splits[i], | |
| .io = file_io_, | |
| .projection = schema, | |
| .properties = reader_properties, | |
| }); | |
| ASSERT_THAT(reader_result, IsOk()); | |
| auto reader = std::move(reader_result.value()); | |
| if (!expected_json[i].empty()) { | |
| ASSERT_NO_FATAL_FAILURE(VerifyNextBatch(*reader, expected_json[i])); | |
| } | |
| ASSERT_NO_FATAL_FAILURE(VerifyExhausted(*reader)); | |
| } | |
| } | |
| TEST_F(ParquetReaderTest, ReadSplitWithoutDictionaryPages) { | |
| ASSERT_NO_FATAL_FAILURE( | |
| CreateSplitParquetFile(/*row_group_size=*/2, /*dictionary=*/false)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto input, | |
| arrow::OpenArrowInputStream(file_io_, temp_parquet_file_)); | |
| auto metadata = ::parquet::ReadMetaData(input); | |
| auto column = metadata->RowGroup(1)->ColumnChunk(0); | |
| ASSERT_FALSE(column->has_dictionary_page()); | |
| const int64_t file_size = input->GetSize().ValueOrDie(); | |
| EXPECT_THAT(RowGroupStart(*metadata->RowGroup(1), file_size), | |
| HasValue(::testing::Eq(column->data_page_offset()))); | |
| ICEBERG_UNWRAP_OR_FAIL(auto midpoint, | |
| RowGroupMidpoint(*metadata->RowGroup(1), file_size)); | |
| auto schema = std::make_shared<Schema>(std::vector<SchemaField>{ | |
| SchemaField::MakeRequired(1, "id", int32()), MetadataColumns::kRowPosition}); | |
| ICEBERG_UNWRAP_OR_FAIL( | |
| auto reader, | |
| ReaderFactoryRegistry::Open( | |
| FileFormatType::kParquet, | |
| {.path = temp_parquet_file_, | |
| .split = Split{.offset = static_cast<size_t>(midpoint), .length = 1}, | |
| .io = file_io_, | |
| .projection = schema})); | |
| ASSERT_NO_FATAL_FAILURE(VerifyNextBatch(*reader, R"([[3, 2]])")); | |
| ASSERT_NO_FATAL_FAILURE(VerifyExhausted(*reader)); | |
| } | |
| TEST_F(ParquetReaderTest, ReadSplitsWithOmittedRowGroupOffsets) { | |
| auto arrow_schema = ::arrow::schema( | |
| {::arrow::field("id", ::arrow::int32(), /*nullable=*/false, | |
| ::arrow::KeyValueMetadata::Make({"PARQUET:field_id"}, {"1"}))}); | |
| auto table = ::arrow::Table::Make( | |
| arrow_schema, | |
| {::arrow::json::ArrayFromJSONString(::arrow::int32(), "[1, 2, 3, 4, 5, 6]") | |
| .ValueOrDie()}); | |
| ::parquet::WriterProperties::Builder properties_builder; | |
| properties_builder.version(::parquet::ParquetVersion::PARQUET_2_6) | |
| ->compression(::parquet::Compression::UNCOMPRESSED); | |
| auto properties = properties_builder.build(); | |
| ICEBERG_UNWRAP_OR_FAIL(auto original_output, | |
| arrow::OpenArrowOutputStream(file_io_, temp_parquet_file_)); | |
| ASSERT_TRUE(::parquet::arrow::WriteTable(*table, ::arrow::default_memory_pool(), | |
| original_output, /*chunk_size=*/2, properties) | |
| .ok()); | |
| ASSERT_TRUE(original_output->Close().ok()); | |
| ICEBERG_UNWRAP_OR_FAIL(auto original_input, | |
| arrow::OpenArrowInputStream(file_io_, temp_parquet_file_)); | |
| auto original_metadata = ::parquet::ReadMetaData(original_input); | |
| auto original_size = original_input->GetSize().ValueOrDie(); | |
| auto pages = original_input->ReadAt(0, original_size - original_metadata->size() - 8) | |
| .ValueOrDie(); | |
| ASSERT_TRUE(original_input->Close().ok()); | |
| auto builder = | |
| ::parquet::FileMetaDataBuilder::Make(original_metadata->schema(), properties); | |
| for (int i = 0; i < original_metadata->num_row_groups(); ++i) { | |
| auto original_row_group = original_metadata->RowGroup(i); | |
| auto row_group = builder->AppendRowGroup(); | |
| row_group->set_num_rows(original_row_group->num_rows()); | |
| for (int j = 0; j < original_row_group->num_columns(); ++j) { | |
| auto column = original_row_group->ColumnChunk(j); | |
| row_group->NextColumnChunk()->Finish( | |
| column->num_values(), column->dictionary_page_offset(), | |
| /*index_page_offset=*/0, column->data_page_offset(), | |
| column->total_compressed_size(), column->total_uncompressed_size(), | |
| column->has_dictionary_page(), /*dictionary_fallback=*/false, {}, {}); | |
| } | |
| // RowGroupMetaDataBuilder::Finish fills in the optional file_offset and | |
| // total_compressed_size. Leave them unset to exercise legacy footer handling. | |
| } | |
| auto rebuilt_metadata = builder->Finish(original_metadata->key_value_metadata()); | |
| ICEBERG_UNWRAP_OR_FAIL(auto output, | |
| arrow::OpenArrowOutputStream(file_io_, temp_parquet_file_)); | |
| ASSERT_TRUE(output->Write(pages).ok()); | |
| ::parquet::WriteFileMetaData(*rebuilt_metadata, output.get()); | |
| ASSERT_TRUE(output->Close().ok()); | |
| ICEBERG_UNWRAP_OR_FAIL(auto input, | |
| arrow::OpenArrowInputStream(file_io_, temp_parquet_file_)); | |
| auto metadata = ::parquet::ReadMetaData(input); | |
| ASSERT_EQ(metadata->num_columns(), 1); | |
| ASSERT_EQ(metadata->num_rows(), 6); | |
| ASSERT_EQ(metadata->num_row_groups(), 3); | |
| for (int i = 0; i < metadata->num_row_groups(); ++i) { | |
| auto row_group = metadata->RowGroup(i); | |
| ASSERT_EQ(row_group->num_rows(), 2); | |
| ASSERT_EQ(row_group->file_offset(), 0); | |
| ASSERT_EQ(row_group->total_compressed_size(), 0); | |
| auto column = row_group->ColumnChunk(0); | |
| ASSERT_TRUE(column->has_dictionary_page()); | |
| ASSERT_LT(column->dictionary_page_offset(), column->data_page_offset()); | |
| } | |
| auto boundary = static_cast<size_t>( | |
| metadata->RowGroup(1)->ColumnChunk(0)->dictionary_page_offset()); | |
| auto file_size = static_cast<size_t>(input->GetSize().ValueOrDie()); | |
| auto schema = std::make_shared<Schema>(std::vector<SchemaField>{ | |
| SchemaField::MakeRequired(1, "id", int32()), MetadataColumns::kRowPosition, | |
| MetadataColumns::kRowId, MetadataColumns::kLastUpdatedSequenceNumber}); | |
| const std::vector<std::optional<Split>> splits{ | |
| std::nullopt, Split{.offset = 0, .length = boundary}, | |
| Split{.offset = boundary, .length = file_size - boundary}}; | |
| const std::vector<std::string> expected{ | |
| R"([[1, 0, 100, 7], [2, 1, 101, 7], [3, 2, 102, 7], [4, 3, 103, 7], [5, 4, 104, 7], [6, 5, 105, 7]])", | |
| R"([[1, 0, 100, 7], [2, 1, 101, 7]])", | |
| R"([[3, 2, 102, 7], [4, 3, 103, 7], [5, 4, 104, 7], [6, 5, 105, 7]])"}; | |
| for (size_t i = 0; i < splits.size(); ++i) { | |
| SCOPED_TRACE(i); | |
| ICEBERG_UNWRAP_OR_FAIL(auto reader, | |
| ReaderFactoryRegistry::Open(FileFormatType::kParquet, | |
| {.path = temp_parquet_file_, | |
| .length = file_size, | |
| .split = splits[i], | |
| .io = file_io_, | |
| .projection = schema, | |
| .first_row_id = 100, | |
| .data_sequence_number = 7})); | |
| ASSERT_NO_FATAL_FAILURE(VerifyNextBatch(*reader, expected[i])); | |
| ASSERT_NO_FATAL_FAILURE(VerifyExhausted(*reader)); | |
| } | |
| } | |
| TEST_F(ParquetReaderTest, RejectsSplitWithInvalidPageOffsetsBeforeReading) { | |
| auto schema = std::make_shared<Schema>( | |
| std::vector<SchemaField>{SchemaField::MakeRequired(1, "col0", int32())}); | |
| for (int64_t offset : {int64_t{-1}, int64_t{0}, std::numeric_limits<int64_t>::max()}) { | |
| auto metadata = MakeMetadataWithPageOffsets({{offset, std::nullopt}}); | |
| ICEBERG_UNWRAP_OR_FAIL(auto output, | |
| arrow::OpenArrowOutputStream(file_io_, temp_parquet_file_)); | |
| ::parquet::WriteMetaDataFile(*metadata, output.get()); | |
| ASSERT_TRUE(output->Close().ok()); | |
| auto reader = ReaderFactoryRegistry::Open(FileFormatType::kParquet, | |
| {.path = temp_parquet_file_, | |
| .split = Split{.offset = 0, .length = 1}, | |
| .io = file_io_, | |
| .projection = schema}); | |
| EXPECT_THAT(reader, IsError(ErrorKind::kInvalid)); | |
| EXPECT_THAT(reader, HasErrorMessage("row group 0")); | |
| EXPECT_THAT(reader, HasErrorMessage(temp_parquet_file_)); | |
| } | |
| } | |
| TEST_F(ParquetReaderTest, ReadSplitWithNonmonotonicRowGroupStarts) { | |
| ASSERT_NO_FATAL_FAILURE(CreateSplitParquetFile(/*row_group_size=*/1)); | |
| // Logical row order becomes [1, 3, 2], while the pages stay in physical order [1, 2, | |
| // 3]. | |
| ASSERT_NO_FATAL_FAILURE(ReorderRowGroups({0, 2, 1})); | |
| ICEBERG_UNWRAP_OR_FAIL(auto input, | |
| arrow::OpenArrowInputStream(file_io_, temp_parquet_file_)); | |
| auto metadata = ::parquet::ReadMetaData(input); | |
| auto last_start = static_cast<size_t>( | |
| metadata->RowGroup(1)->ColumnChunk(0)->dictionary_page_offset()); | |
| auto schema = std::make_shared<Schema>(std::vector<SchemaField>{ | |
| SchemaField::MakeRequired(1, "id", int32()), MetadataColumns::kRowPosition, | |
| MetadataColumns::kRowId, MetadataColumns::kLastUpdatedSequenceNumber}); | |
| ICEBERG_UNWRAP_OR_FAIL( | |
| auto reader, | |
| ReaderFactoryRegistry::Open(FileFormatType::kParquet, | |
| {.path = temp_parquet_file_, | |
| .split = Split{.offset = 0, .length = last_start}, | |
| .io = file_io_, | |
| .projection = schema, | |
| .first_row_id = 100, | |
| .data_sequence_number = 7})); | |
| // A batch can span skipped row groups while keeping absolute positions and lineage. | |
| ASSERT_NO_FATAL_FAILURE( | |
| VerifyNextBatch(*reader, R"([[1, 0, 100, 7], [2, 2, 102, 7]])")); | |
| ASSERT_NO_FATAL_FAILURE(VerifyExhausted(*reader)); | |
| ICEBERG_UNWRAP_OR_FAIL( | |
| auto tail, ReaderFactoryRegistry::Open( | |
| FileFormatType::kParquet, | |
| {.path = temp_parquet_file_, | |
| .split = Split{.offset = last_start, | |
| .length = std::numeric_limits<size_t>::max()}, | |
| .io = file_io_, | |
| .projection = schema, | |
| .first_row_id = 100, | |
| .data_sequence_number = 7})); | |
| ASSERT_NO_FATAL_FAILURE(VerifyNextBatch(*tail, R"([[3, 1, 101, 7]])")); | |
| ASSERT_NO_FATAL_FAILURE(VerifyExhausted(*tail)); | |
| } | |
| TEST_F(ParquetReaderTest, ReadMetadataOnlySplitWithNonmonotonicRowGroups) { | |
| ASSERT_NO_FATAL_FAILURE(CreateSplitParquetFile(/*row_group_size=*/1)); | |
| ASSERT_NO_FATAL_FAILURE(ReorderRowGroups({0, 2, 1})); | |
| ICEBERG_UNWRAP_OR_FAIL(auto input, | |
| arrow::OpenArrowInputStream(file_io_, temp_parquet_file_)); | |
| auto metadata = ::parquet::ReadMetaData(input); | |
| auto last_start = static_cast<size_t>( | |
| metadata->RowGroup(1)->ColumnChunk(0)->dictionary_page_offset()); | |
| auto schema = std::make_shared<Schema>( | |
| std::vector<SchemaField>{MetadataColumns::kRowPosition, MetadataColumns::kRowId, | |
| MetadataColumns::kLastUpdatedSequenceNumber}); | |
| for (int64_t batch_size : {1, 100}) { | |
| SCOPED_TRACE(batch_size); | |
| ReaderProperties properties; | |
| properties.Set(ReaderProperties::kBatchSize, batch_size); | |
| ICEBERG_UNWRAP_OR_FAIL( | |
| auto reader, | |
| ReaderFactoryRegistry::Open(FileFormatType::kParquet, | |
| {.path = temp_parquet_file_, | |
| .split = Split{.offset = 0, .length = last_start}, | |
| .io = file_io_, | |
| .projection = schema, | |
| .first_row_id = 100, | |
| .data_sequence_number = 7, | |
| .properties = properties})); | |
| // Arrow emits one batch per row group when no physical columns are read. | |
| ASSERT_NO_FATAL_FAILURE(VerifyNextBatch(*reader, R"([[0, 100, 7]])")); | |
| ASSERT_NO_FATAL_FAILURE(VerifyNextBatch(*reader, R"([[2, 102, 7]])")); | |
| ASSERT_NO_FATAL_FAILURE(VerifyExhausted(*reader)); | |
| } | |
| } | |
| TEST_F(ParquetReaderTest, ReadMetadataColumns) { | |
| temp_parquet_file_ = "metadata_columns.parquet"; | |
| CreateSimpleParquetFile(); | |
| // Create schema with both _file and _pos metadata columns | |
| auto schema = std::make_shared<Schema>(std::vector<SchemaField>{ | |
| SchemaField::MakeRequired(1, "id", int32()), | |
| MetadataColumns::kFilePath, | |
| MetadataColumns::kRowPosition, | |
| }); | |
| const std::string kExpectedJson = | |
| R"([[1, "metadata_columns.parquet", 0], | |
| [2, "metadata_columns.parquet", 1], | |
| [3, "metadata_columns.parquet", 2]])"; | |
| ICEBERG_UNWRAP_OR_FAIL( | |
| auto reader, | |
| ReaderFactoryRegistry::Open( | |
| FileFormatType::kParquet, | |
| {.path = temp_parquet_file_, .io = file_io_, .projection = schema})); | |
| ASSERT_NO_FATAL_FAILURE(VerifyNextBatch(*reader, kExpectedJson)); | |
| } | |
| TEST_F(ParquetReaderTest, ReadRowPositionWithBatchSize) { | |
| CreateSimpleParquetFile(); | |
| // Create schema with _pos metadata column | |
| auto schema = std::make_shared<Schema>(std::vector<SchemaField>{ | |
| SchemaField::MakeRequired(1, "id", int32()), | |
| MetadataColumns::kRowPosition, | |
| }); | |
| ReaderProperties reader_properties; | |
| reader_properties.Set(ReaderProperties::kBatchSize, int64_t{2}); | |
| ICEBERG_UNWRAP_OR_FAIL(auto reader, ReaderFactoryRegistry::Open( | |
| FileFormatType::kParquet, | |
| {.path = temp_parquet_file_, | |
| .io = file_io_, | |
| .projection = schema, | |
| .properties = std::move(reader_properties)})); | |
| ASSERT_NO_FATAL_FAILURE(VerifyNextBatch(*reader, R"([[1, 0], [2, 1]])")); | |
| ASSERT_NO_FATAL_FAILURE(VerifyNextBatch(*reader, R"([[3, 2]])")); | |
| ASSERT_NO_FATAL_FAILURE(VerifyExhausted(*reader)); | |
| } | |
| TEST_F(ParquetReaderTest, ReadMetadataOnlyProjection) { | |
| temp_parquet_file_ = "metadata_only.parquet"; | |
| CreateSimpleParquetFile(); | |
| // Create schema with only metadata columns (no data columns) | |
| auto schema = std::make_shared<Schema>(std::vector<SchemaField>{ | |
| MetadataColumns::kFilePath, | |
| MetadataColumns::kRowPosition, | |
| }); | |
| const std::string kExpectedJson = | |
| R"([["metadata_only.parquet", 0], | |
| ["metadata_only.parquet", 1], | |
| ["metadata_only.parquet", 2]])"; | |
| ICEBERG_UNWRAP_OR_FAIL( | |
| auto reader, | |
| ReaderFactoryRegistry::Open( | |
| FileFormatType::kParquet, | |
| {.path = temp_parquet_file_, .io = file_io_, .projection = schema})); | |
| ASSERT_NO_FATAL_FAILURE(VerifyNextBatch(*reader, kExpectedJson)); | |
| } | |
| TEST_F(ParquetReaderTest, ReadRowLineage) { | |
| temp_parquet_file_ = "row_lineage.parquet"; | |
| CreateSimpleParquetFile(); | |
| ICEBERG_UNWRAP_OR_FAIL(auto reader, OpenRowLineageReader(RowLineageSchema(), 100L, 7L)); | |
| ASSERT_NO_FATAL_FAILURE(VerifyNextBatch(*reader, R"([[1, 100, 7], | |
| [2, 101, 7], | |
| [3, 102, 7]])")); | |
| } | |
| TEST_F(ParquetReaderTest, ReadPartialLineage) { | |
| temp_parquet_file_ = "partial_lineage.parquet"; | |
| CreateSimpleParquetFile(); | |
| auto schema = RowLineageSchema(); | |
| ICEBERG_UNWRAP_OR_FAIL(auto reader, OpenRowLineageReader(schema)); | |
| ASSERT_NO_FATAL_FAILURE(VerifyNextBatch(*reader, R"([[1, null, null], | |
| [2, null, null], | |
| [3, null, null]])")); | |
| ICEBERG_UNWRAP_OR_FAIL(auto reader_with_row_id, OpenRowLineageReader(schema, 100L)); | |
| ASSERT_NO_FATAL_FAILURE(VerifyNextBatch(*reader_with_row_id, | |
| R"([[1, 100, null], | |
| [2, 101, null], | |
| [3, 102, null]])")); | |
| ICEBERG_UNWRAP_OR_FAIL(auto reader_with_sequence, | |
| OpenRowLineageReader(schema, std::nullopt, 7L)); | |
| ASSERT_NO_FATAL_FAILURE(VerifyNextBatch(*reader_with_sequence, | |
| R"([[1, null, 7], | |
| [2, null, 7], | |
| [3, null, 7]])")); | |
| } | |
| TEST_F(ParquetReaderTest, ReadPhysicalLineage) { | |
| temp_parquet_file_ = "physical_lineage.parquet"; | |
| CreateRowLineageParquetFile(); | |
| auto schema = RowLineageSchema(); | |
| ICEBERG_UNWRAP_OR_FAIL(auto reader, OpenRowLineageReader(schema, 100L)); | |
| ASSERT_NO_FATAL_FAILURE(VerifyNextBatch(*reader, R"([[1, 100, null], | |
| [2, 777, 8], | |
| [3, 102, null]])")); | |
| } | |
| TEST_F(ParquetReaderTest, ReadPositionsAndLineageAcrossRowGroups) { | |
| temp_parquet_file_ = "positions_across_row_groups.parquet"; | |
| constexpr int64_t kRowGroupRows = 100; | |
| constexpr int64_t kNumRows = 3 * kRowGroupRows; | |
| constexpr int64_t kFirstRowId = 1000; | |
| // Odd rows keep a physical _row_id; even rows inherit `first_row_id + _pos`. | |
| std::string input_json = "["; | |
| for (int64_t i = 0; i < kNumRows; ++i) { | |
| input_json += std::format("{}[{}, {}, null]", i == 0 ? "" : ",", i, | |
| i % 2 == 0 ? "null" : std::to_string(5000 + i)); | |
| } | |
| input_json += "]"; | |
| auto write_schema = RowLineageSchema(); | |
| ArrowSchema arrow_c_schema; | |
| ASSERT_THAT(ToArrowSchema(*write_schema, &arrow_c_schema), IsOk()); | |
| auto arrow_type = ::arrow::ImportType(&arrow_c_schema).ValueOrDie(); | |
| auto array = ::arrow::json::ArrayFromJSONString(arrow_type, input_json).ValueOrDie(); | |
| WriterProperties writer_properties; | |
| writer_properties.Set(WriterProperties::kParquetCompression, | |
| std::string("uncompressed")); | |
| writer_properties.Set(WriterProperties::kParquetMaxRowGroupRows, kRowGroupRows); | |
| ASSERT_THAT(WriteArray(array, {.path = temp_parquet_file_, | |
| .schema = write_schema, | |
| .io = file_io_, | |
| .properties = std::move(writer_properties)}), | |
| IsOk()); | |
| auto io = internal::checked_cast<arrow::ArrowFileSystemFileIO&>(*file_io_); | |
| auto metadata = | |
| ::parquet::ReadMetaData(io.fs()->OpenInputFile(temp_parquet_file_).ValueOrDie()); | |
| ASSERT_EQ(metadata->num_row_groups(), 3); | |
| auto read_schema = std::make_shared<Schema>(std::vector<SchemaField>{ | |
| SchemaField::MakeRequired(1, "id", int32()), | |
| MetadataColumns::kRowPosition, | |
| MetadataColumns::kRowId, | |
| }); | |
| auto expected_json = [&](int64_t start, int64_t end) { | |
| std::string json = "["; | |
| for (int64_t pos = start; pos < end; ++pos) { | |
| json += std::format("{}[{}, {}, {}]", pos == start ? "" : ",", pos, pos, | |
| pos % 2 == 0 ? kFirstRowId + pos : 5000 + pos); | |
| } | |
| return json + "]"; | |
| }; | |
| ReaderProperties reader_properties; | |
| reader_properties.Set(ReaderProperties::kBatchSize, int64_t{150}); | |
| // Skip the first row group; the first batch spans the second and third ones. | |
| const auto second_offset = static_cast<size_t>(metadata->RowGroup(1)->file_offset()); | |
| ICEBERG_UNWRAP_OR_FAIL( | |
| auto reader, | |
| ReaderFactoryRegistry::Open( | |
| FileFormatType::kParquet, | |
| {.path = temp_parquet_file_, | |
| .split = Split{.offset = second_offset, | |
| .length = std::numeric_limits<size_t>::max() - second_offset}, | |
| .io = file_io_, | |
| .projection = read_schema, | |
| .first_row_id = kFirstRowId, | |
| .properties = reader_properties})); | |
| ASSERT_NO_FATAL_FAILURE(VerifyNextBatch(*reader, expected_json(100, 250))); | |
| ASSERT_NO_FATAL_FAILURE(VerifyNextBatch(*reader, expected_json(250, 300))); | |
| ASSERT_NO_FATAL_FAILURE(VerifyExhausted(*reader)); | |
| // Read only the middle row group. | |
| const auto third_offset = static_cast<size_t>(metadata->RowGroup(2)->file_offset()); | |
| ICEBERG_UNWRAP_OR_FAIL( | |
| auto middle_reader, | |
| ReaderFactoryRegistry::Open(FileFormatType::kParquet, | |
| {.path = temp_parquet_file_, | |
| .split = Split{.offset = second_offset, | |
| .length = third_offset - second_offset}, | |
| .io = file_io_, | |
| .projection = read_schema, | |
| .first_row_id = kFirstRowId, | |
| .properties = reader_properties})); | |
| ASSERT_NO_FATAL_FAILURE(VerifyNextBatch(*middle_reader, expected_json(100, 200))); | |
| ASSERT_NO_FATAL_FAILURE(VerifyExhausted(*middle_reader)); | |
| } | |
| TEST_F(ParquetReaderTest, ReadNestedUnknownProjection) { | |
| temp_parquet_file_ = "nested_unknown.parquet"; | |
| auto write_schema = std::make_shared<Schema>(std::vector<SchemaField>{ | |
| SchemaField::MakeOptional(1, "profile", | |
| std::make_shared<StructType>(std::vector<SchemaField>{ | |
| SchemaField::MakeOptional(2, "name", string()), | |
| SchemaField::MakeOptional(3, "mystery", int32()), | |
| })), | |
| SchemaField::MakeOptional( | |
| 4, "mysteries", | |
| std::make_shared<ListType>(SchemaField::MakeOptional(5, "element", int32()))), | |
| SchemaField::MakeOptional( | |
| 6, "properties", | |
| std::make_shared<MapType>(SchemaField::MakeRequired(7, "key", string()), | |
| SchemaField::MakeOptional(8, "value", int32()))), | |
| SchemaField::MakeOptional(9, "wrapper", | |
| std::make_shared<StructType>(std::vector<SchemaField>{ | |
| SchemaField::MakeOptional(10, "mystery", int32()), | |
| })), | |
| }); | |
| auto read_schema = std::make_shared<Schema>(std::vector<SchemaField>{ | |
| SchemaField::MakeOptional(1, "profile", | |
| std::make_shared<StructType>(std::vector<SchemaField>{ | |
| SchemaField::MakeOptional(2, "name", string()), | |
| SchemaField::MakeOptional(3, "mystery", unknown()), | |
| })), | |
| SchemaField::MakeOptional( | |
| 4, "mysteries", | |
| std::make_shared<ListType>(SchemaField::MakeOptional(5, "element", unknown()))), | |
| SchemaField::MakeOptional( | |
| 6, "properties", | |
| std::make_shared<MapType>(SchemaField::MakeRequired(7, "key", string()), | |
| SchemaField::MakeOptional(8, "value", unknown()))), | |
| SchemaField::MakeOptional(9, "wrapper", | |
| std::make_shared<StructType>(std::vector<SchemaField>{ | |
| SchemaField::MakeOptional(10, "mystery", unknown()), | |
| })), | |
| }); | |
| ArrowSchema arrow_c_schema; | |
| ASSERT_THAT(ToArrowSchema(*write_schema, &arrow_c_schema), IsOk()); | |
| auto arrow_type = ::arrow::ImportType(&arrow_c_schema).ValueOrDie(); | |
| auto array = ::arrow::json::ArrayFromJSONString(arrow_type, | |
| R"([ | |
| {"profile": {"name": "Person0", "mystery": 10}, "mysteries": [1, 2], "properties": [["a", 100], ["b", 200]], "wrapper": {"mystery": 300}}, | |
| {"profile": {"name": "Person1", "mystery": null}, "mysteries": [], "properties": [], "wrapper": {"mystery": null}} | |
| ])") | |
| .ValueOrDie(); | |
| WriterProperties writer_properties; | |
| writer_properties.Set(WriterProperties::kParquetCompression, | |
| std::string("uncompressed")); | |
| ASSERT_THAT(WriteArray(array, {.path = temp_parquet_file_, | |
| .schema = write_schema, | |
| .io = file_io_, | |
| .properties = std::move(writer_properties)}), | |
| IsOk()); | |
| ICEBERG_UNWRAP_OR_FAIL( | |
| auto reader, | |
| ReaderFactoryRegistry::Open( | |
| FileFormatType::kParquet, | |
| {.path = temp_parquet_file_, .io = file_io_, .projection = read_schema})); | |
| ASSERT_NO_FATAL_FAILURE(VerifyNextBatch(*reader, | |
| R"([ | |
| {"profile": {"name": "Person0", "mystery": null}, "mysteries": [null, null], "properties": [["a", null], ["b", null]], "wrapper": {"mystery": null}}, | |
| {"profile": {"name": "Person1", "mystery": null}, "mysteries": [], "properties": [], "wrapper": {"mystery": null}} | |
| ])")); | |
| ASSERT_NO_FATAL_FAILURE(VerifyExhausted(*reader)); | |
| } | |
| class ParquetReadWrite : public ::testing::Test { | |
| protected: | |
| static void SetUpTestSuite() { parquet::RegisterAll(); } | |
| }; | |
| TEST_F(ParquetReadWrite, EmptyStruct) { | |
| auto schema = | |
| std::make_shared<Schema>(std::vector<SchemaField>{SchemaField::MakeRequired( | |
| 1, "empty_struct", std::make_shared<StructType>(std::vector<SchemaField>{}))}); | |
| ArrowSchema arrow_c_schema; | |
| ASSERT_THAT(ToArrowSchema(*schema, &arrow_c_schema), IsOk()); | |
| auto arrow_schema = ::arrow::ImportType(&arrow_c_schema).ValueOrDie(); | |
| auto array = ::arrow::json::ArrayFromJSONString( | |
| ::arrow::struct_(arrow_schema->fields()), R"([null, {}])") | |
| .ValueOrDie(); | |
| std::shared_ptr<FileIO> file_io = arrow::ArrowFileSystemFileIO::MakeMockFileIO(); | |
| const std::string basePath = "base.parquet"; | |
| ASSERT_THAT(WriteArray(array, {.path = basePath, .schema = schema, .io = file_io}), | |
| IsError(ErrorKind::kNotImplemented)); | |
| } | |
| TEST_F(ParquetReadWrite, RejectsUnavailableCompressionCodec) { | |
| auto unavailable_codec = FirstUnavailableParquetCodec(); | |
| if (!unavailable_codec.has_value()) { | |
| GTEST_SKIP() << "All optional Parquet compression codecs are available"; | |
| } | |
| auto schema = std::make_shared<Schema>( | |
| std::vector<SchemaField>{SchemaField::MakeRequired(1, "id", int32())}); | |
| WriterProperties writer_properties; | |
| writer_properties.Set(WriterProperties::kParquetCompression, unavailable_codec->name); | |
| auto writer = WriterFactoryRegistry::Open( | |
| FileFormatType::kParquet, {.path = "unavailable_codec.parquet", | |
| .schema = schema, | |
| .io = arrow::ArrowFileSystemFileIO::MakeMockFileIO(), | |
| .properties = std::move(writer_properties)}); | |
| EXPECT_THAT(writer, IsError(ErrorKind::kInvalidArgument)); | |
| EXPECT_THAT(writer, | |
| HasErrorMessage("Parquet compression codec " + unavailable_codec->name + | |
| " is not available in the current build")); | |
| } | |
| TEST_F(ParquetReadWrite, WritesUnknownFieldsNestedInsideListOrMapStructs) { | |
| auto schema = std::make_shared<Schema>(std::vector<SchemaField>{ | |
| SchemaField::MakeOptional(1, "id", int32()), | |
| SchemaField::MakeOptional(2, "events", | |
| std::make_shared<ListType>(SchemaField::MakeOptional( | |
| 3, ListType::kElementName, | |
| std::make_shared<StructType>(std::vector<SchemaField>{ | |
| SchemaField::MakeOptional(4, "name", string()), | |
| SchemaField::MakeOptional(5, "secret", unknown()), | |
| })))), | |
| SchemaField::MakeOptional( | |
| 6, "properties", | |
| std::make_shared<MapType>( | |
| SchemaField::MakeRequired(7, MapType::kKeyName, iceberg::string()), | |
| SchemaField::MakeOptional( | |
| 8, MapType::kValueName, | |
| std::make_shared<StructType>(std::vector<SchemaField>{ | |
| SchemaField::MakeOptional(9, "label", string()), | |
| SchemaField::MakeOptional(10, "secret", unknown()), | |
| })))), | |
| }); | |
| ArrowSchema arrow_c_schema; | |
| ASSERT_THAT(ToArrowSchema(*schema, &arrow_c_schema), IsOk()); | |
| auto arrow_schema = ::arrow::ImportType(&arrow_c_schema).ValueOrDie(); | |
| auto array = | |
| ::arrow::json::ArrayFromJSONString(::arrow::struct_(arrow_schema->fields()), | |
| R"([ | |
| {"id": 1, "events": [{"name": "open", "secret": null}, {"name": "close", "secret": null}], "properties": [["a", {"label": "A", "secret": null}]]}, | |
| {"id": 2, "events": [], "properties": []} | |
| ])") | |
| .ValueOrDie(); | |
| std::shared_ptr<FileIO> file_io = arrow::ArrowFileSystemFileIO::MakeMockFileIO(); | |
| const std::string basePath = "nested_unknown_fields.parquet"; | |
| WriterProperties writer_properties; | |
| writer_properties.Set(WriterProperties::kParquetCompression, | |
| std::string("uncompressed")); | |
| ASSERT_THAT(WriteArray(array, {.path = basePath, | |
| .schema = schema, | |
| .io = file_io, | |
| .properties = std::move(writer_properties)}), | |
| IsOk()); | |
| auto& arrow_file_io = internal::checked_cast<arrow::ArrowFileSystemFileIO&>(*file_io); | |
| auto input_file = arrow_file_io.fs()->OpenInputFile(basePath).ValueOrDie(); | |
| auto parquet_reader = ::parquet::ParquetFileReader::Open(input_file); | |
| auto parquet_schema = parquet_reader->metadata()->schema(); | |
| std::vector<int32_t> field_ids; | |
| for (int i = 0; i < parquet_schema->num_columns(); ++i) { | |
| field_ids.push_back(parquet_schema->Column(i)->schema_node()->field_id()); | |
| } | |
| // Unknown fields (secret, IDs 5 and 10) are also written as null-type columns. | |
| EXPECT_THAT(field_ids, ::testing::UnorderedElementsAre(1, 4, 5, 7, 9, 10)); | |
| std::shared_ptr<::arrow::Array> out; | |
| ASSERT_THAT(ReadArray(out, {.path = basePath, .io = file_io, .projection = schema}, | |
| /*metadata=*/nullptr), | |
| IsOk()); | |
| auto expected = | |
| ::arrow::json::ArrayFromJSONString(::arrow::struct_(arrow_schema->fields()), | |
| R"([ | |
| {"id": 1, "events": [{"name": "open", "secret": null}, {"name": "close", "secret": null}], "properties": [["a", {"label": "A", "secret": null}]]}, | |
| {"id": 2, "events": [], "properties": []} | |
| ])") | |
| .ValueOrDie(); | |
| ASSERT_TRUE(out->Equals(*expected)) << "actual:\n" | |
| << out->ToString() << "expected:\n" | |
| << expected->ToString(); | |
| } | |
| TEST_F(ParquetReadWrite, DoesNotMaterializeUnknownFieldsOnWrite) { | |
| auto schema = std::make_shared<Schema>(std::vector<SchemaField>{ | |
| SchemaField::MakeOptional(1, "id", int32()), | |
| SchemaField::MakeOptional(2, "mystery", unknown()), | |
| SchemaField::MakeOptional(3, "profile", | |
| std::make_shared<StructType>(std::vector<SchemaField>{ | |
| SchemaField::MakeOptional(4, "name", string()), | |
| SchemaField::MakeOptional(5, "secret", unknown()), | |
| })), | |
| }); | |
| ArrowSchema arrow_c_schema; | |
| ASSERT_THAT(ToArrowSchema(*schema, &arrow_c_schema), IsOk()); | |
| auto arrow_schema = ::arrow::ImportType(&arrow_c_schema).ValueOrDie(); | |
| auto array = | |
| ::arrow::json::ArrayFromJSONString(::arrow::struct_(arrow_schema->fields()), | |
| R"([ | |
| [1, null, {"name": "Person0", "secret": null}], | |
| [2, null, {"name": "Person1", "secret": null}] | |
| ])") | |
| .ValueOrDie(); | |
| std::shared_ptr<FileIO> file_io = arrow::ArrowFileSystemFileIO::MakeMockFileIO(); | |
| const std::string basePath = "unknown_fields.parquet"; | |
| WriterProperties writer_properties; | |
| writer_properties.Set(WriterProperties::kParquetCompression, | |
| std::string("uncompressed")); | |
| ASSERT_THAT(WriteArray(array, {.path = basePath, | |
| .schema = schema, | |
| .io = file_io, | |
| .properties = std::move(writer_properties)}), | |
| IsOk()); | |
| auto& arrow_file_io = internal::checked_cast<arrow::ArrowFileSystemFileIO&>(*file_io); | |
| auto input_file = arrow_file_io.fs()->OpenInputFile(basePath).ValueOrDie(); | |
| auto parquet_reader = ::parquet::ParquetFileReader::Open(input_file); | |
| auto parquet_schema = parquet_reader->metadata()->schema(); | |
| // Unknown fields (mystery, secret) are also written as null-type columns. | |
| ASSERT_EQ(parquet_schema->num_columns(), 4); | |
| EXPECT_EQ(parquet_schema->Column(0)->schema_node()->field_id(), 1); | |
| EXPECT_EQ(parquet_schema->Column(1)->schema_node()->field_id(), 2); | |
| EXPECT_EQ(parquet_schema->Column(2)->schema_node()->field_id(), 4); | |
| EXPECT_EQ(parquet_schema->Column(3)->schema_node()->field_id(), 5); | |
| std::shared_ptr<::arrow::Array> out; | |
| ASSERT_THAT(ReadArray(out, {.path = basePath, .io = file_io, .projection = schema}, | |
| /*metadata=*/nullptr), | |
| IsOk()); | |
| auto expected = | |
| ::arrow::json::ArrayFromJSONString(::arrow::struct_(arrow_schema->fields()), | |
| R"([ | |
| [1, null, {"name": "Person0", "secret": null}], | |
| [2, null, {"name": "Person1", "secret": null}] | |
| ])") | |
| .ValueOrDie(); | |
| ASSERT_TRUE(out->Equals(*expected)) << "actual:\n" | |
| << out->ToString() << "expected:\n" | |
| << expected->ToString(); | |
| } | |
| TEST_F(ParquetReadWrite, SimpleStructRoundTrip) { | |
| auto schema = std::make_shared<Schema>(std::vector<SchemaField>{ | |
| SchemaField::MakeOptional(1, "a", | |
| struct_({ | |
| SchemaField::MakeOptional(2, "a1", int64()), | |
| SchemaField::MakeOptional(3, "a2", string()), | |
| })), | |
| }); | |
| ArrowSchema arrow_c_schema; | |
| ASSERT_THAT(ToArrowSchema(*schema, &arrow_c_schema), IsOk()); | |
| auto arrow_schema = ::arrow::ImportType(&arrow_c_schema).ValueOrDie(); | |
| auto array = ::arrow::json::ArrayFromJSONString( | |
| ::arrow::struct_(arrow_schema->fields()), | |
| R"([[{"a1": 1, "a2": "abc"}], [{"a1": 0}], [{"a2": "edf"}], [{}]])") | |
| .ValueOrDie(); | |
| std::shared_ptr<::arrow::Array> out; | |
| DoRoundtrip(array, schema, out); | |
| ASSERT_TRUE(out->Equals(*array)); | |
| } | |
| TEST_F(ParquetReadWrite, SimpleTypeRoundTrip) { | |
| auto schema = std::make_shared<Schema>(std::vector<SchemaField>{ | |
| SchemaField::MakeOptional(1, "a", boolean()), | |
| SchemaField::MakeOptional(2, "b", int32()), | |
| SchemaField::MakeOptional(3, "c", int64()), | |
| SchemaField::MakeOptional(4, "d", float32()), | |
| SchemaField::MakeOptional(5, "e", float64()), | |
| SchemaField::MakeOptional(6, "f", string()), | |
| SchemaField::MakeOptional(7, "g", time()), | |
| SchemaField::MakeOptional(8, "h", timestamp()), | |
| }); | |
| ArrowSchema arrow_c_schema; | |
| ASSERT_THAT(ToArrowSchema(*schema, &arrow_c_schema), IsOk()); | |
| auto arrow_schema = ::arrow::ImportType(&arrow_c_schema).ValueOrDie(); | |
| auto array = ::arrow::json::ArrayFromJSONString( | |
| ::arrow::struct_(arrow_schema->fields()), | |
| R"([[true, 1, 2, 1.1, 1.2, "abc", 44614000, 1756696503000000], | |
| [false, 0, 0, 0, 0, "", 0, 0], | |
| [null, null, null, null, null, null, null, null]])") | |
| .ValueOrDie(); | |
| std::shared_ptr<::arrow::Array> out; | |
| DoRoundtrip(array, schema, out); | |
| ASSERT_TRUE(out->Equals(*array)); | |
| } | |
| TEST_F(ParquetReadWrite, UuidRoundTrip) { | |
| auto schema = std::make_shared<Schema>( | |
| std::vector<SchemaField>{SchemaField::MakeRequired(1, "uuid_col", uuid())}); | |
| ::arrow::FixedSizeBinaryBuilder uuid_storage_builder( | |
| ::arrow::fixed_size_binary(Uuid::kLength)); | |
| ASSERT_TRUE(uuid_storage_builder.Append(kUuidBytes1.data()).ok()); | |
| ASSERT_TRUE(uuid_storage_builder.Append(kUuidBytes2.data()).ok()); | |
| auto uuid_storage = uuid_storage_builder.Finish().ValueOrDie(); | |
| auto uuid_array = | |
| ::arrow::ExtensionType::WrapArray(::arrow::extension::uuid(), uuid_storage); | |
| auto array = | |
| ::arrow::StructArray::Make( | |
| {uuid_array}, | |
| {::arrow::field("uuid_col", ::arrow::extension::uuid(), /*nullable=*/false)}) | |
| .ValueOrDie(); | |
| std::shared_ptr<::arrow::Array> out; | |
| DoRoundtrip(array, schema, out); | |
| ASSERT_TRUE(out->Equals(*array)) << "actual:\n" | |
| << out->ToString() << "\nexpected:\n" | |
| << array->ToString(); | |
| } | |
| TEST_F(ParquetReadWrite, GeospatialWkbRoundTrip) { | |
| auto schema = std::make_shared<Schema>(std::vector<SchemaField>{ | |
| SchemaField::MakeOptional(1, "geom", geometry()), | |
| SchemaField::MakeOptional(2, "geog", | |
| geography("EPSG:4326", EdgeAlgorithm::kAndoyer)), | |
| SchemaField::MakeOptional( | |
| 3, "nested", | |
| struct_({SchemaField::MakeOptional(4, "geom", geometry("EPSG:3857"))})), | |
| }); | |
| ::arrow::BinaryBuilder geometry_builder; | |
| const std::array<uint8_t, 4> arbitrary_geometry = {0xff, 0x00, 0x7f, 0x42}; | |
| ASSERT_TRUE( | |
| geometry_builder.Append(arbitrary_geometry.data(), arbitrary_geometry.size()).ok()); | |
| ASSERT_TRUE(geometry_builder.AppendNull().ok()); | |
| ::arrow::BinaryBuilder geography_builder; | |
| const std::array<uint8_t, 5> arbitrary_geography = {0x01, 0x02, 0x03, 0x04, 0x05}; | |
| ASSERT_TRUE(geography_builder.AppendNull().ok()); | |
| ASSERT_TRUE( | |
| geography_builder.Append(arbitrary_geography.data(), arbitrary_geography.size()) | |
| .ok()); | |
| ::arrow::BinaryBuilder nested_geometry_builder; | |
| const std::array<uint8_t, 3> arbitrary_nested_geometry = {0xde, 0xad, 0xbe}; | |
| ASSERT_TRUE( | |
| nested_geometry_builder | |
| .Append(arbitrary_nested_geometry.data(), arbitrary_nested_geometry.size()) | |
| .ok()); | |
| ASSERT_TRUE(nested_geometry_builder.AppendNull().ok()); | |
| auto nested = | |
| ::arrow::StructArray::Make({nested_geometry_builder.Finish().ValueOrDie()}, | |
| {::arrow::field("geom", ::arrow::binary())}) | |
| .ValueOrDie(); | |
| auto array = | |
| ::arrow::StructArray::Make({geometry_builder.Finish().ValueOrDie(), | |
| geography_builder.Finish().ValueOrDie(), nested}, | |
| {::arrow::field("geom", ::arrow::binary()), | |
| ::arrow::field("geog", ::arrow::binary()), | |
| ::arrow::field("nested", nested->type())}) | |
| .ValueOrDie(); | |
| std::shared_ptr<::arrow::Array> out; | |
| DoRoundtrip(array, schema, out); | |
| ASSERT_NE(out, nullptr); | |
| ASSERT_TRUE(out->Equals(*array)) << "actual:\n" | |
| << out->ToString() << "\nexpected:\n" | |
| << array->ToString(); | |
| } | |
| } // namespace iceberg::parquet |