blob: 057a2716c30af03c7a58c695d82440abb0eb99f7 [file]
/*
* 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.
*/
/// \file arrow_row_builder_test.cc
/// Unit tests for ArrowRowBuilder and its typed append helpers.
#include <memory>
#include <vector>
#include <arrow/array.h>
#include <arrow/c/bridge.h>
#include <arrow/record_batch.h>
#include <gtest/gtest.h>
#include "iceberg/arrow_row_builder_internal.h"
#include "iceberg/schema.h"
#include "iceberg/schema_field.h"
#include "iceberg/schema_internal.h"
#include "iceberg/test/matchers.h"
#include "iceberg/type.h"
namespace iceberg {
namespace {
/// \brief A schema exercising every append helper: int32, string, int64,
/// boolean, and map<string, string>.
std::shared_ptr<Schema> MakeTestSchema() {
return std::make_shared<Schema>(std::vector<SchemaField>{
SchemaField::MakeRequired(1, "id", int32()),
SchemaField::MakeOptional(2, "name", string()),
SchemaField::MakeOptional(3, "count", int64()),
SchemaField::MakeOptional(4, "active", boolean()),
SchemaField::MakeOptional(
5, "props",
std::make_shared<MapType>(SchemaField::MakeRequired(6, "key", string()),
SchemaField::MakeRequired(7, "value", string())))});
}
/// \brief A schema exercising the list/map append helpers backing Iceberg's
/// optional DataFile fields (e.g. equality_ids, split_offsets, column_sizes,
/// lower_bounds): list<int32>, list<int64>, map<int32, int64>, and
/// map<int32, binary>.
std::shared_ptr<Schema> MakeListMapTestSchema() {
return std::make_shared<Schema>(std::vector<SchemaField>{
SchemaField::MakeOptional(1, "int32_list",
list(SchemaField::MakeRequired(
2, std::string(ListType::kElementName), int32()))),
SchemaField::MakeOptional(3, "int64_list",
list(SchemaField::MakeRequired(
4, std::string(ListType::kElementName), int64()))),
SchemaField::MakeOptional(
5, "int_map",
map(SchemaField::MakeRequired(6, std::string(MapType::kKeyName), int32()),
SchemaField::MakeRequired(7, std::string(MapType::kValueName), int64()))),
SchemaField::MakeOptional(
8, "binary_map",
map(SchemaField::MakeRequired(9, std::string(MapType::kKeyName), int32()),
SchemaField::MakeRequired(10, std::string(MapType::kValueName),
binary())))});
}
/// \brief Finish a builder and import the result into an Arrow RecordBatch.
std::shared_ptr<::arrow::RecordBatch> FinishAndImport(ArrowRowBuilder builder,
const Schema& schema) {
auto array_result = std::move(builder).Finish();
EXPECT_THAT(array_result, IsOk());
ArrowSchema c_schema;
EXPECT_THAT(ToArrowSchema(schema, &c_schema), IsOk());
auto arrow_schema = ::arrow::ImportSchema(&c_schema).ValueOrDie();
// ImportRecordBatch takes ownership of the array and releases it.
return ::arrow::ImportRecordBatch(&array_result.value(), arrow_schema).ValueOrDie();
}
} // namespace
TEST(ArrowRowBuilderTest, BuildsRowsWithTypedValues) {
auto schema = MakeTestSchema();
ICEBERG_UNWRAP_OR_FAIL(auto builder, ArrowRowBuilder::Make(*schema));
ASSERT_EQ(builder.num_columns(), 5);
ASSERT_EQ(builder.num_rows(), 0);
// Row 0
ASSERT_THAT(AppendInt(builder.column(0), 1), IsOk());
ASSERT_THAT(AppendString(builder.column(1), "alice"), IsOk());
ASSERT_THAT(AppendInt(builder.column(2), 100), IsOk());
ASSERT_THAT(AppendBoolean(builder.column(3), true), IsOk());
ASSERT_THAT(AppendStringMap(builder.column(4), {{"k", "v"}}), IsOk());
ASSERT_THAT(builder.FinishRow(), IsOk());
ASSERT_EQ(builder.num_rows(), 1);
// Row 1
ASSERT_THAT(AppendInt(builder.column(0), 2), IsOk());
ASSERT_THAT(AppendString(builder.column(1), "bob"), IsOk());
ASSERT_THAT(AppendInt(builder.column(2), 200), IsOk());
ASSERT_THAT(AppendBoolean(builder.column(3), false), IsOk());
ASSERT_THAT(AppendStringMap(builder.column(4), {}), IsOk());
ASSERT_THAT(builder.FinishRow(), IsOk());
ASSERT_EQ(builder.num_rows(), 2);
auto batch = FinishAndImport(std::move(builder), *schema);
ASSERT_EQ(batch->num_rows(), 2);
ASSERT_EQ(batch->num_columns(), 5);
auto id = std::static_pointer_cast<::arrow::Int32Array>(batch->column(0));
EXPECT_EQ(id->Value(0), 1);
EXPECT_EQ(id->Value(1), 2);
auto name = std::static_pointer_cast<::arrow::StringArray>(batch->column(1));
EXPECT_EQ(name->GetString(0), "alice");
EXPECT_EQ(name->GetString(1), "bob");
auto count = std::static_pointer_cast<::arrow::Int64Array>(batch->column(2));
EXPECT_EQ(count->Value(0), 100);
EXPECT_EQ(count->Value(1), 200);
auto active = std::static_pointer_cast<::arrow::BooleanArray>(batch->column(3));
EXPECT_TRUE(active->Value(0));
EXPECT_FALSE(active->Value(1));
// props: one entry in row 0, empty (but non-null) map in row 1.
auto props = std::static_pointer_cast<::arrow::MapArray>(batch->column(4));
EXPECT_FALSE(props->IsNull(0));
EXPECT_FALSE(props->IsNull(1));
EXPECT_EQ(props->value_length(0), 1);
EXPECT_EQ(props->value_length(1), 0);
}
TEST(ArrowRowBuilderTest, AppendsNullForOptionalColumns) {
auto schema = MakeTestSchema();
ICEBERG_UNWRAP_OR_FAIL(auto builder, ArrowRowBuilder::Make(*schema));
ASSERT_THAT(AppendInt(builder.column(0), 42), IsOk());
ASSERT_THAT(AppendNull(builder.column(1)), IsOk());
ASSERT_THAT(AppendNull(builder.column(2)), IsOk());
ASSERT_THAT(AppendNull(builder.column(3)), IsOk());
ASSERT_THAT(AppendStringMap(builder.column(4), {}), IsOk());
ASSERT_THAT(builder.FinishRow(), IsOk());
auto batch = FinishAndImport(std::move(builder), *schema);
ASSERT_EQ(batch->num_rows(), 1);
auto id = std::static_pointer_cast<::arrow::Int32Array>(batch->column(0));
EXPECT_FALSE(id->IsNull(0));
EXPECT_EQ(id->Value(0), 42);
EXPECT_TRUE(batch->column(1)->IsNull(0));
EXPECT_TRUE(batch->column(2)->IsNull(0));
EXPECT_TRUE(batch->column(3)->IsNull(0));
}
TEST(ArrowRowBuilderTest, AppendsMultiEntryStringMap) {
auto schema = MakeTestSchema();
ICEBERG_UNWRAP_OR_FAIL(auto builder, ArrowRowBuilder::Make(*schema));
ASSERT_THAT(AppendInt(builder.column(0), 1), IsOk());
ASSERT_THAT(AppendNull(builder.column(1)), IsOk());
ASSERT_THAT(AppendNull(builder.column(2)), IsOk());
ASSERT_THAT(AppendNull(builder.column(3)), IsOk());
ASSERT_THAT(AppendStringMap(builder.column(4), {{"a", "1"}, {"b", "2"}, {"c", "3"}}),
IsOk());
ASSERT_THAT(builder.FinishRow(), IsOk());
auto batch = FinishAndImport(std::move(builder), *schema);
auto props = std::static_pointer_cast<::arrow::MapArray>(batch->column(4));
EXPECT_EQ(props->value_length(0), 3);
}
TEST(ArrowRowBuilderTest, EmptyBuilderProducesZeroRowBatch) {
auto schema = MakeTestSchema();
ICEBERG_UNWRAP_OR_FAIL(auto builder, ArrowRowBuilder::Make(*schema));
auto batch = FinishAndImport(std::move(builder), *schema);
EXPECT_EQ(batch->num_rows(), 0);
EXPECT_EQ(batch->num_columns(), 5);
}
TEST(ArrowRowBuilderTest, ColumnIndexOutOfRangeReturnsNull) {
auto schema = MakeTestSchema();
ICEBERG_UNWRAP_OR_FAIL(auto builder, ArrowRowBuilder::Make(*schema));
EXPECT_EQ(builder.num_columns(), 5);
EXPECT_NE(builder.column(0), nullptr);
EXPECT_NE(builder.column(4), nullptr);
EXPECT_EQ(builder.column(-1), nullptr);
EXPECT_EQ(builder.column(5), nullptr);
}
TEST(ArrowRowBuilderTest, AppendIntListWritesNullForEmptyInput) {
auto schema = MakeListMapTestSchema();
ICEBERG_UNWRAP_OR_FAIL(auto builder, ArrowRowBuilder::Make(*schema));
// Row 0: non-empty lists.
ASSERT_THAT(AppendIntList(builder.column(0), std::vector<int32_t>{1, 2}), IsOk());
ASSERT_THAT(AppendIntList(builder.column(1), std::vector<int64_t>{3, 4}), IsOk());
ASSERT_THAT(AppendNull(builder.column(2)), IsOk());
ASSERT_THAT(AppendNull(builder.column(3)), IsOk());
ASSERT_THAT(builder.FinishRow(), IsOk());
// Row 1: empty lists must be encoded as null, not an empty list.
ASSERT_THAT(AppendIntList(builder.column(0), std::vector<int32_t>{}), IsOk());
ASSERT_THAT(AppendIntList(builder.column(1), std::vector<int64_t>{}), IsOk());
ASSERT_THAT(AppendNull(builder.column(2)), IsOk());
ASSERT_THAT(AppendNull(builder.column(3)), IsOk());
ASSERT_THAT(builder.FinishRow(), IsOk());
auto batch = FinishAndImport(std::move(builder), *schema);
ASSERT_EQ(batch->num_rows(), 2);
auto int32_list = std::static_pointer_cast<::arrow::ListArray>(batch->column(0));
EXPECT_FALSE(int32_list->IsNull(0));
EXPECT_TRUE(int32_list->IsNull(1));
auto int64_list = std::static_pointer_cast<::arrow::ListArray>(batch->column(1));
EXPECT_FALSE(int64_list->IsNull(0));
EXPECT_TRUE(int64_list->IsNull(1));
}
TEST(ArrowRowBuilderTest, AppendIntMapWritesNullForEmptyInput) {
auto schema = MakeListMapTestSchema();
ICEBERG_UNWRAP_OR_FAIL(auto builder, ArrowRowBuilder::Make(*schema));
// Row 0: a non-empty map.
ASSERT_THAT(AppendNull(builder.column(0)), IsOk());
ASSERT_THAT(AppendNull(builder.column(1)), IsOk());
ASSERT_THAT(AppendIntMap(builder.column(2), {{1, 100}}), IsOk());
ASSERT_THAT(AppendNull(builder.column(3)), IsOk());
ASSERT_THAT(builder.FinishRow(), IsOk());
// Row 1: an empty map must be encoded as null, not an empty map.
ASSERT_THAT(AppendNull(builder.column(0)), IsOk());
ASSERT_THAT(AppendNull(builder.column(1)), IsOk());
ASSERT_THAT(AppendIntMap(builder.column(2), {}), IsOk());
ASSERT_THAT(AppendNull(builder.column(3)), IsOk());
ASSERT_THAT(builder.FinishRow(), IsOk());
auto batch = FinishAndImport(std::move(builder), *schema);
ASSERT_EQ(batch->num_rows(), 2);
auto int_map = std::static_pointer_cast<::arrow::MapArray>(batch->column(2));
EXPECT_FALSE(int_map->IsNull(0));
EXPECT_TRUE(int_map->IsNull(1));
}
TEST(ArrowRowBuilderTest, AppendBinaryMapWritesNullForEmptyInput) {
auto schema = MakeListMapTestSchema();
ICEBERG_UNWRAP_OR_FAIL(auto builder, ArrowRowBuilder::Make(*schema));
// Row 0: a non-empty map.
ASSERT_THAT(AppendNull(builder.column(0)), IsOk());
ASSERT_THAT(AppendNull(builder.column(1)), IsOk());
ASSERT_THAT(AppendNull(builder.column(2)), IsOk());
ASSERT_THAT(AppendBinaryMap(builder.column(3), {{1, {0x01, 0x02}}}), IsOk());
ASSERT_THAT(builder.FinishRow(), IsOk());
// Row 1: an empty map must be encoded as null, not an empty map.
ASSERT_THAT(AppendNull(builder.column(0)), IsOk());
ASSERT_THAT(AppendNull(builder.column(1)), IsOk());
ASSERT_THAT(AppendNull(builder.column(2)), IsOk());
ASSERT_THAT(AppendBinaryMap(builder.column(3), {}), IsOk());
ASSERT_THAT(builder.FinishRow(), IsOk());
auto batch = FinishAndImport(std::move(builder), *schema);
ASSERT_EQ(batch->num_rows(), 2);
auto binary_map = std::static_pointer_cast<::arrow::MapArray>(batch->column(3));
EXPECT_FALSE(binary_map->IsNull(0));
EXPECT_TRUE(binary_map->IsNull(1));
}
} // namespace iceberg