blob: cddfce69f8c6426e2cb134868d7ae5b40f4e4996 [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.
*/
#include "iceberg/inspect/snapshots_table.h"
#include <cstdint>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include <arrow/array.h>
#include <arrow/c/bridge.h>
#include <arrow/record_batch.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "iceberg/constants.h"
#include "iceberg/inspect/metadata_table.h"
#include "iceberg/test/matchers.h"
#include "iceberg/test/metadata_table_test_base.h"
namespace iceberg {
namespace {
std::vector<std::pair<std::string, std::string>> GetMapEntries(
const std::shared_ptr<::arrow::MapArray>& map_array, int64_t row) {
auto keys = std::static_pointer_cast<::arrow::StringArray>(map_array->keys());
auto values = std::static_pointer_cast<::arrow::StringArray>(map_array->items());
std::vector<std::pair<std::string, std::string>> entries;
entries.reserve(map_array->value_length(row));
const auto offset = map_array->value_offset(row);
for (int64_t index = offset; index < offset + map_array->value_length(row); ++index) {
entries.emplace_back(keys->GetString(index), values->GetString(index));
}
return entries;
}
} // namespace
class SnapshotsTableTest : public MetadataTableTestBase {
protected:
void SetUp() override {
MetadataTableTestBase::SetUp();
auto [snap1, snap2] = MakeTestSnapshots();
ICEBERG_UNWRAP_OR_FAIL(
table_, MakeTableWithSnapshots({snap1, snap2}, /*current_snapshot_id=*/2));
ICEBERG_UNWRAP_OR_FAIL(snapshots_table_, MetadataTable::Make<SnapshotsTable>(table_));
}
std::unique_ptr<MetadataTable> snapshots_table_;
};
TEST_F(SnapshotsTableTest, Construct) {
EXPECT_EQ(snapshots_table_->kind(), MetadataTable::Kind::kSnapshots);
EXPECT_EQ(snapshots_table_->source_table(), table_);
EXPECT_NE(snapshots_table_->schema(), nullptr);
}
TEST_F(SnapshotsTableTest, Scan) {
// Scan the snapshots table once and verify all columns of the result.
ICEBERG_UNWRAP_OR_FAIL(auto stream, snapshots_table_->Scan());
ICEBERG_UNWRAP_OR_FAIL(auto batches, ReadAllBatches(std::move(stream)));
ASSERT_EQ(batches.size(), 1);
const auto& batch = batches.front();
// Row and column counts.
EXPECT_EQ(batch->num_rows(), 2);
EXPECT_EQ(batch->num_columns(), 6);
// Column 0: committed_at (timestamptz) — microseconds since epoch.
auto committed_at = std::static_pointer_cast<::arrow::TimestampArray>(batch->column(0));
EXPECT_EQ(committed_at->Value(0), 1234567890000LL * 1000);
EXPECT_EQ(committed_at->Value(1), 9876543210000LL * 1000);
// Column 1: snapshot_id (long) — returned in storage order.
auto snapshot_ids = std::static_pointer_cast<::arrow::Int64Array>(batch->column(1));
EXPECT_EQ(snapshot_ids->Value(0), 1);
EXPECT_EQ(snapshot_ids->Value(1), 2);
// Column 2: parent_id (long) — first snapshot has no parent.
auto parent_ids = std::static_pointer_cast<::arrow::Int64Array>(batch->column(2));
EXPECT_TRUE(parent_ids->IsNull(0));
EXPECT_FALSE(parent_ids->IsNull(1));
EXPECT_EQ(parent_ids->Value(1), 1);
// Column 3: operation (string).
auto operations = std::static_pointer_cast<::arrow::StringArray>(batch->column(3));
EXPECT_EQ(operations->GetString(0), "append");
EXPECT_EQ(operations->GetString(1), "append");
// Column 4: manifest_list (string).
auto manifest_lists = std::static_pointer_cast<::arrow::StringArray>(batch->column(4));
EXPECT_EQ(manifest_lists->GetString(0), "file:/tmp/manifest1.avro");
EXPECT_EQ(manifest_lists->GetString(1), "file:/tmp/manifest2.avro");
// Column 5: summary (map<string,string>) excludes the separate operation field.
auto summaries = std::static_pointer_cast<::arrow::MapArray>(batch->column(5));
EXPECT_FALSE(summaries->IsNull(0));
EXPECT_FALSE(summaries->IsNull(1));
EXPECT_EQ(summaries->value_length(0), 10);
EXPECT_EQ(summaries->value_length(1), 10);
auto first_summary = GetMapEntries(summaries, 0);
EXPECT_THAT(
first_summary,
::testing::Not(::testing::Contains(::testing::Pair("operation", "append"))));
EXPECT_THAT(first_summary, ::testing::Contains(::testing::Pair("total-records", "1")));
auto second_summary = GetMapEntries(summaries, 1);
EXPECT_THAT(
second_summary,
::testing::Not(::testing::Contains(::testing::Pair("operation", "append"))));
EXPECT_THAT(second_summary, ::testing::Contains(::testing::Pair("total-records", "2")));
}
TEST_F(SnapshotsTableTest, ScanEmptySnapshotList) {
// A table with zero snapshots should return zero rows.
ICEBERG_UNWRAP_OR_FAIL(
auto empty_table,
MakeTableWithSnapshots({}, /*current_snapshot_id=*/kInvalidSnapshotId));
ICEBERG_UNWRAP_OR_FAIL(snapshots_table_,
MetadataTable::Make<SnapshotsTable>(empty_table));
ICEBERG_UNWRAP_OR_FAIL(auto stream, snapshots_table_->Scan());
ICEBERG_UNWRAP_OR_FAIL(auto batches, ReadAllBatches(std::move(stream)));
EXPECT_TRUE(batches.empty());
}
TEST_F(SnapshotsTableTest, ScanSkipsNullSnapshots) {
auto [snap1, snap2] = MakeTestSnapshots();
ICEBERG_UNWRAP_OR_FAIL(auto table, MakeTableWithSnapshots({snap1, nullptr, snap2},
/*current_snapshot_id=*/2));
ICEBERG_UNWRAP_OR_FAIL(auto snapshots_table,
MetadataTable::Make<SnapshotsTable>(table));
ICEBERG_UNWRAP_OR_FAIL(auto stream, snapshots_table->Scan());
ICEBERG_UNWRAP_OR_FAIL(auto batches, ReadAllBatches(std::move(stream)));
ASSERT_EQ(batches.size(), 1);
EXPECT_EQ(batches.front()->num_rows(), 2);
}
TEST_F(SnapshotsTableTest, ScanEmptySummary) {
auto [missing_summary, operation_only_summary] = MakeTestSnapshots();
missing_summary->summary.clear();
operation_only_summary->summary = {
{SnapshotSummaryFields::kOperation, DataOperation::kAppend}};
ICEBERG_UNWRAP_OR_FAIL(auto table,
MakeTableWithSnapshots({missing_summary, operation_only_summary},
/*current_snapshot_id=*/2));
ICEBERG_UNWRAP_OR_FAIL(auto snapshots_table,
MetadataTable::Make<SnapshotsTable>(table));
ICEBERG_UNWRAP_OR_FAIL(auto stream, snapshots_table->Scan());
ICEBERG_UNWRAP_OR_FAIL(auto batches, ReadAllBatches(std::move(stream)));
ASSERT_EQ(batches.size(), 1);
auto summaries =
std::static_pointer_cast<::arrow::MapArray>(batches.front()->column(5));
EXPECT_TRUE(summaries->IsNull(0));
EXPECT_FALSE(summaries->IsNull(1));
EXPECT_EQ(summaries->value_length(1), 0);
}
TEST_F(SnapshotsTableTest, ScanReturnsMultipleBatches) {
auto snapshot = MakeTestSnapshots().first;
std::vector<std::shared_ptr<Snapshot>> snapshots(MetadataTable::kBatchSize + 1,
snapshot);
ICEBERG_UNWRAP_OR_FAIL(auto table, MakeTableWithSnapshots(std::move(snapshots),
/*current_snapshot_id=*/1));
ICEBERG_UNWRAP_OR_FAIL(auto snapshots_table,
MetadataTable::Make<SnapshotsTable>(table));
ICEBERG_UNWRAP_OR_FAIL(auto stream, snapshots_table->Scan());
ICEBERG_UNWRAP_OR_FAIL(auto batches, ReadAllBatches(std::move(stream)));
ASSERT_EQ(batches.size(), 2);
EXPECT_EQ(batches[0]->num_rows(), MetadataTable::kBatchSize);
EXPECT_EQ(batches[1]->num_rows(), 1);
}
TEST_F(SnapshotsTableTest, ScanUsesMetadataCapturedAtCreation) {
auto snapshot = MakeTestSnapshots().first;
std::vector<std::shared_ptr<Snapshot>> snapshots(MetadataTable::kBatchSize + 1,
snapshot);
ICEBERG_UNWRAP_OR_FAIL(auto table, MakeTableWithSnapshots(std::move(snapshots),
/*current_snapshot_id=*/1));
ICEBERG_UNWRAP_OR_FAIL(auto snapshots_table,
MetadataTable::Make<SnapshotsTable>(table));
ICEBERG_UNWRAP_OR_FAIL(auto stream, snapshots_table->Scan());
auto reader_result = ::arrow::ImportRecordBatchReader(&stream);
ASSERT_TRUE(reader_result.ok()) << reader_result.status().ToString();
auto reader = std::move(reader_result).ValueUnsafe();
auto first_batch_result = reader->Next();
ASSERT_TRUE(first_batch_result.ok()) << first_batch_result.status().ToString();
ASSERT_NE(*first_batch_result, nullptr);
EXPECT_EQ((*first_batch_result)->num_rows(), MetadataTable::kBatchSize);
ICEBERG_UNWRAP_OR_FAIL(
auto refreshed_table,
MakeTableWithSnapshots({}, /*current_snapshot_id=*/kInvalidSnapshotId,
"s3://bucket/refreshed-meta.json"));
EXPECT_CALL(*catalog_, LoadTable(table->name()))
.WillOnce(::testing::Return(refreshed_table));
ASSERT_THAT(table->Refresh(), IsOk());
auto second_batch_result = reader->Next();
ASSERT_TRUE(second_batch_result.ok()) << second_batch_result.status().ToString();
ASSERT_NE(*second_batch_result, nullptr);
EXPECT_EQ((*second_batch_result)->num_rows(), 1);
}
} // namespace iceberg