This is an automated email from the ASF dual-hosted git repository.
wgtmac pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-cpp.git
The following commit(s) were added to refs/heads/main by this push:
new 49b84a06 feat(inspect): add task-based scans for snapshots and history
(#1066)
49b84a06 is described below
commit 49b84a0621ef952095aa8ba52bb08fe25ee85976
Author: Gang Wu <[email protected]>
AuthorDate: Tue Nov 3 22:32:45 2026 +0800
feat(inspect): add task-based scans for snapshots and history (#1066)
- Add Table::Inspect() and task-based metadata scans while retaining
column selection, projection, case sensitivity, and time-travel
builder configuration from the merged inspection API.
- Separate task planning from reading with reusable static tasks backed
by captured table metadata.
- Preserve scan configuration and metadata across builder changes and
table refreshes.
- Produce Arrow batches lazily through MetadataScanTaskReader, with
reader-controlled batch sizes and independent stream lifetimes.
- Keep canonical schemas and row conversion in each metadata table
implementation, sharing batch construction and projection validation.
- Consolidate projected-column append helpers with lazy batch construction.
- Preserve all merged metadata inspection test scenarios and cover planned
projections, task reuse, stream lifetimes, batching, and vector<bool>.
---
src/iceberg/CMakeLists.txt | 2 +
src/iceberg/cpp20_compatibility_internal.h | 2 +
src/iceberg/inspect/history_table.cc | 127 ++++++++++-
src/iceberg/inspect/history_table.h | 38 +++-
src/iceberg/inspect/metadata_scan.cc | 143 ++++++++++--
src/iceberg/inspect/metadata_scan.h | 135 ++++++------
src/iceberg/inspect/metadata_scan_task.cc | 51 +++++
src/iceberg/inspect/metadata_scan_task.h | 88 ++++++++
src/iceberg/inspect/metadata_scan_task_reader.cc | 68 ++++++
src/iceberg/inspect/metadata_scan_task_reader.h | 61 +++++
...ns_internal.h => metadata_scan_util_internal.h} | 87 +++++++-
src/iceberg/inspect/metadata_table.cc | 43 ++--
src/iceberg/inspect/metadata_table.h | 113 ++++------
src/iceberg/inspect/snapshots_table.cc | 245 +++++++++------------
src/iceberg/inspect/snapshots_table.h | 44 +++-
src/iceberg/table.cc | 5 +
src/iceberg/table.h | 3 +
src/iceberg/test/arrow_test_util.h | 87 ++++++++
src/iceberg/test/history_table_test.cc | 171 ++++++++++++--
src/iceberg/test/metadata_table_test.cc | 163 +++++---------
src/iceberg/test/metadata_table_test_base.h | 11 +-
src/iceberg/test/snapshots_table_test.cc | 70 +++++-
src/iceberg/test/stream_test.cc | 37 ++++
src/iceberg/type_fwd.h | 7 +
src/iceberg/util/stream.h | 28 +++
25 files changed, 1342 insertions(+), 487 deletions(-)
diff --git a/src/iceberg/CMakeLists.txt b/src/iceberg/CMakeLists.txt
index cf465e5f..eee72061 100644
--- a/src/iceberg/CMakeLists.txt
+++ b/src/iceberg/CMakeLists.txt
@@ -61,6 +61,8 @@ set(ICEBERG_SOURCES
inspect/history_table.cc
inspect/metadata_scan.cc
inspect/metadata_table.cc
+ inspect/metadata_scan_task.cc
+ inspect/metadata_scan_task_reader.cc
inspect/snapshots_table.cc
inheritable_metadata.cc
json_serde.cc
diff --git a/src/iceberg/cpp20_compatibility_internal.h
b/src/iceberg/cpp20_compatibility_internal.h
index d186e5bb..6bcc5b74 100644
--- a/src/iceberg/cpp20_compatibility_internal.h
+++ b/src/iceberg/cpp20_compatibility_internal.h
@@ -82,6 +82,8 @@
#include "iceberg/inheritable_metadata.h"
#include "iceberg/inspect/history_table.h"
#include "iceberg/inspect/metadata_scan.h"
+#include "iceberg/inspect/metadata_scan_task.h"
+#include "iceberg/inspect/metadata_scan_task_reader.h"
#include "iceberg/inspect/metadata_table.h"
#include "iceberg/inspect/snapshots_table.h"
#include "iceberg/labels.h"
diff --git a/src/iceberg/inspect/history_table.cc
b/src/iceberg/inspect/history_table.cc
index 518073ca..a6f616fd 100644
--- a/src/iceberg/inspect/history_table.cc
+++ b/src/iceberg/inspect/history_table.cc
@@ -19,24 +19,67 @@
#include "iceberg/inspect/history_table.h"
+#include <array>
#include <memory>
+#include <unordered_map>
+#include <unordered_set>
#include <utility>
-#include <vector>
+#include "iceberg/arrow_row_builder_internal.h"
+#include "iceberg/constants.h"
+#include "iceberg/inspect/metadata_scan_task.h"
+#include "iceberg/inspect/metadata_scan_util_internal.h"
#include "iceberg/schema.h"
#include "iceberg/schema_field.h"
+#include "iceberg/snapshot.h"
#include "iceberg/table.h"
+#include "iceberg/table_metadata.h"
#include "iceberg/type.h"
#include "iceberg/util/macros.h"
namespace iceberg {
+namespace {
-HistoryTable::HistoryTable(std::shared_ptr<Table> table)
+struct HistoryRow {
+ const SnapshotLogEntry& entry;
+ const Snapshot* snapshot;
+ bool is_current_ancestor;
+};
+
+constexpr std::array<internal::MetadataTableColumn<HistoryRow>, 4>
kHistoryColumns = {
+ {{.field_id = 1,
+ .append =
+ [](ArrowArray* column, const HistoryRow& row) {
+ return AppendInt(column,
+ UnixMsFromTimePointMs(row.entry.timestamp_ms) *
1000);
+ }},
+ {.field_id = 2,
+ .append =
+ [](ArrowArray* column, const HistoryRow& row) {
+ return AppendInt(column, row.entry.snapshot_id);
+ }},
+ {.field_id = 3,
+ .append =
+ [](ArrowArray* column, const HistoryRow& row) {
+ return row.snapshot != nullptr && row.snapshot->parent_snapshot_id
+ ? AppendInt(column, *row.snapshot->parent_snapshot_id)
+ : AppendNull(column);
+ }},
+ {.field_id = 4, .append = [](ArrowArray* column, const HistoryRow& row) {
+ return AppendBoolean(column, row.is_current_ancestor);
+ }}}};
+
+} // namespace
+
+// HistoryTable implementation
+HistoryTable::HistoryTable(std::shared_ptr<const Table> table)
: MetadataTable(std::move(table)) {}
HistoryTable::~HistoryTable() = default;
-const std::shared_ptr<Schema>& HistoryTable::schema() const {
+const std::shared_ptr<Schema>& HistoryTable::schema() const { return
CanonicalSchema(); }
+
+const std::shared_ptr<Schema>& HistoryTable::CanonicalSchema() {
static const auto schema = std::make_shared<Schema>(std::vector<SchemaField>{
SchemaField::MakeRequired(1, "made_current_at", timestamp_tz()),
SchemaField::MakeRequired(2, "snapshot_id", int64()),
@@ -45,14 +88,84 @@ const std::shared_ptr<Schema>& HistoryTable::schema() const
{
return schema;
}
-Result<std::unique_ptr<HistoryTable>>
HistoryTable::Make(std::shared_ptr<Table> table) {
+Result<std::unique_ptr<HistoryTable>> HistoryTable::Make(
+ std::shared_ptr<const Table> table) {
ICEBERG_PRECHECK(table != nullptr, "Table cannot be null");
return std::unique_ptr<HistoryTable>(new HistoryTable(std::move(table)));
}
-Result<ArrowArrayStream> HistoryTable::ScanProjection(
- const Schema& /*projected_schema*/) const {
- return NotSupported("Scan is not supported for the history table");
+Result<std::unique_ptr<MetadataScan>> HistoryTable::MakeScan(
+ internal::MetadataScanContext context) const {
+ return HistoryScan::Make(std::move(context));
+}
+
+Result<ArrowArrayStream> HistoryTable::ReadRows(
+ std::shared_ptr<const TableMetadata> metadata,
+ const std::shared_ptr<Schema>& projected_schema, int64_t batch_size) {
+ std::unordered_map<int64_t, const Snapshot*> snapshots_by_id;
+ for (const auto& snapshot : metadata->snapshots) {
+ if (snapshot) {
+ snapshots_by_id.emplace(snapshot->snapshot_id, snapshot.get());
+ }
+ }
+
+ // Walk the current snapshot's ancestry; the insert check stops on cycles.
+ std::unordered_set<int64_t> ancestors;
+ for (auto current = metadata->current_snapshot_id; current !=
kInvalidSnapshotId;) {
+ auto it = snapshots_by_id.find(current);
+ // Stop at a missing snapshot without adding its ID to the ancestors.
+ if (it == snapshots_by_id.end() || !ancestors.insert(current).second) {
+ break;
+ }
+ if (!it->second->parent_snapshot_id) {
+ break;
+ }
+ current = *it->second->parent_snapshot_id;
+ }
+
+ const size_t num_rows = metadata->snapshot_log.size();
+ return internal::MakeMetadataBatchStream<HistoryRow>(
+ CanonicalSchema(), projected_schema, kHistoryColumns, num_rows,
batch_size,
+ [metadata = std::move(metadata), snapshots_by_id =
std::move(snapshots_by_id),
+ ancestors = std::move(ancestors)](size_t index) -> HistoryRow {
+ const auto& entry = metadata->snapshot_log[index];
+ auto it = snapshots_by_id.find(entry.snapshot_id);
+ return {.entry = entry,
+ .snapshot = it != snapshots_by_id.end() ? it->second : nullptr,
+ .is_current_ancestor = ancestors.contains(entry.snapshot_id)};
+ });
+}
+
+// HistoryScan implementation
+HistoryScan::HistoryScan(internal::MetadataScanContext context,
+ std::shared_ptr<MetadataScanTask> task)
+ : MetadataScan(std::move(context)), task_(std::move(task)) {}
+
+HistoryScan::~HistoryScan() = default;
+
+Result<std::unique_ptr<HistoryScan>> HistoryScan::Make(
+ internal::MetadataScanContext context) {
+ ICEBERG_PRECHECK(context.kind == MetadataTableKind::kHistory,
+ "Unexpected metadata table kind for history scan");
+ if (context.snapshot_id.has_value() || !context.ref_name.empty() ||
+ context.as_of_time_in_millis.has_value()) {
+ return NotSupported("Metadata table does not support time travel");
+ }
+ ICEBERG_PRECHECK(context.table_metadata != nullptr, "Table metadata cannot
be null");
+ ICEBERG_PRECHECK(context.schema != nullptr, "Metadata table schema cannot be
null");
+ ICEBERG_PRECHECK(context.projected_schema != nullptr,
+ "Projected schema cannot be null");
+ ICEBERG_RETURN_UNEXPECTED(internal::SelectAppendFns<HistoryRow>(
+ *HistoryTable::CanonicalSchema(), *context.projected_schema,
kHistoryColumns));
+ ICEBERG_ASSIGN_OR_RAISE(auto task,
+ StaticMetadataTask::Make(context.kind,
context.table_metadata,
+ context.projected_schema));
+ return std::unique_ptr<HistoryScan>(
+ new HistoryScan(std::move(context), std::move(task)));
+}
+
+Result<MetadataScanTaskStreamPtr> HistoryScan::PlanTasksStream() const {
+ return MetadataScanTaskStream::FromVector({task_});
}
} // namespace iceberg
diff --git a/src/iceberg/inspect/history_table.h
b/src/iceberg/inspect/history_table.h
index 6f07bcd0..ec18e36a 100644
--- a/src/iceberg/inspect/history_table.h
+++ b/src/iceberg/inspect/history_table.h
@@ -22,9 +22,12 @@
/// \file iceberg/inspect/history_table.h
/// \brief Define the history metadata table.
+#include <cstdint>
#include <memory>
+#include "iceberg/arrow_c_data.h"
#include "iceberg/iceberg_export.h"
+#include "iceberg/inspect/metadata_scan.h"
#include "iceberg/inspect/metadata_table.h"
#include "iceberg/result.h"
#include "iceberg/type_fwd.h"
@@ -34,19 +37,46 @@ namespace iceberg {
/// \brief History metadata table.
class ICEBERG_EXPORT HistoryTable : public MetadataTable {
public:
- static Result<std::unique_ptr<HistoryTable>> Make(std::shared_ptr<Table>
table);
+ static Result<std::unique_ptr<HistoryTable>> Make(std::shared_ptr<const
Table> table);
+
+ /// \brief Return the canonical schema of the history metadata table.
+ static const std::shared_ptr<Schema>& CanonicalSchema();
~HistoryTable() override;
- Kind kind() const noexcept override { return Kind::kHistory; }
+ MetadataTableKind kind() const noexcept override { return
MetadataTableKind::kHistory; }
const std::shared_ptr<Schema>& schema() const override;
protected:
- Result<ArrowArrayStream> ScanProjection(const Schema& projected_schema)
const override;
+ Result<std::unique_ptr<MetadataScan>> MakeScan(
+ internal::MetadataScanContext context) const override;
private:
- explicit HistoryTable(std::shared_ptr<Table> table);
+ friend class MetadataScanTaskReader;
+
+ static Result<ArrowArrayStream> ReadRows(
+ std::shared_ptr<const TableMetadata> metadata,
+ const std::shared_ptr<Schema>& projected_schema, int64_t batch_size);
+
+ explicit HistoryTable(std::shared_ptr<const Table> table);
+};
+
+/// \brief Scan the history metadata captured from a table.
+class ICEBERG_EXPORT HistoryScan final : public MetadataScan {
+ public:
+ /// \brief Create a scan from captured metadata table settings.
+ static Result<std::unique_ptr<HistoryScan>>
Make(internal::MetadataScanContext context);
+
+ ~HistoryScan() override;
+
+ Result<MetadataScanTaskStreamPtr> PlanTasksStream() const override;
+
+ private:
+ HistoryScan(internal::MetadataScanContext context,
+ std::shared_ptr<MetadataScanTask> task);
+
+ const std::shared_ptr<MetadataScanTask> task_;
};
} // namespace iceberg
diff --git a/src/iceberg/inspect/metadata_scan.cc
b/src/iceberg/inspect/metadata_scan.cc
index 6117c28c..2957a2ba 100644
--- a/src/iceberg/inspect/metadata_scan.cc
+++ b/src/iceberg/inspect/metadata_scan.cc
@@ -19,25 +19,115 @@
#include "iceberg/inspect/metadata_scan.h"
-#include <memory>
+#include <optional>
#include <utility>
+#include "iceberg/arrow_c_data_util_internal.h"
+#include "iceberg/inspect/metadata_scan_task_reader.h"
#include "iceberg/inspect/metadata_table.h"
#include "iceberg/schema.h"
+#include "iceberg/schema_internal.h"
+#include "iceberg/table.h"
#include "iceberg/util/macros.h"
+#include "iceberg/util/stream.h"
namespace iceberg {
+namespace {
+
+class TaskArrowStreamSource {
+ public:
+ TaskArrowStreamSource(std::vector<std::shared_ptr<MetadataScanTask>> tasks,
+ std::unique_ptr<MetadataScanTaskReader> reader,
+ std::shared_ptr<iceberg::Schema> schema)
+ : tasks_(std::move(tasks)),
+ reader_(std::move(reader)),
+ schema_(std::move(schema)) {}
+
+ ~TaskArrowStreamSource() { std::ignore = Close(); }
+
+ Status Close() {
+ if (current_.release != nullptr) {
+ current_.release(¤t_);
+ }
+ return {};
+ }
+
+ Result<ArrowSchema> Schema() {
+ ArrowSchema schema{};
+ ICEBERG_RETURN_UNEXPECTED(ToArrowSchema(*schema_, &schema));
+ return schema;
+ }
+
+ Result<std::optional<ArrowArray>> Next() {
+ if (error_) {
+ return iceberg::unexpected(*error_);
+ }
+ auto result = NextBatch();
+ if (!result) {
+ error_ = result.error();
+ }
+ return result;
+ }
+
+ Result<std::optional<ArrowArray>> NextBatch() {
+ while (true) {
+ if (current_.release == nullptr) {
+ if (next_task_ == tasks_.size()) {
+ return std::nullopt;
+ }
+ const auto& task = tasks_[next_task_++];
+ ICEBERG_CHECK(task != nullptr, "Metadata scan task cannot be null");
+ ICEBERG_ASSIGN_OR_RAISE(current_, reader_->Open(*task));
+ }
+
+ ArrowArray array{};
+ const int error = current_.get_next(¤t_, &array);
+ if (error != 0) {
+ std::string message = current_.get_last_error != nullptr &&
+ current_.get_last_error(¤t_) !=
nullptr
+ ? current_.get_last_error(¤t_)
+ : "Metadata task stream failed";
+ current_.release(¤t_);
+ return IOError("{}", message);
+ }
+ if (array.release != nullptr) {
+ return array;
+ }
+ current_.release(¤t_);
+ }
+ }
+
+ const std::vector<std::shared_ptr<MetadataScanTask>> tasks_;
+ const std::unique_ptr<MetadataScanTaskReader> reader_;
+ const std::shared_ptr<iceberg::Schema> schema_;
+ size_t next_task_ = 0;
+ ArrowArrayStream current_{};
+ std::optional<Error> error_;
+};
+
+} // namespace
MetadataScanBuilder::MetadataScanBuilder(const MetadataTable& table)
- : table_(&table), projected_schema_(table.schema()) {}
+ : table_(&table),
+ context_{.kind = table.kind(),
+ .table_metadata = table.source_table()->metadata(),
+ .schema = table.schema(),
+ .projected_schema = table.schema()} {}
MetadataScanBuilder::~MetadataScanBuilder() = default;
+Result<std::unique_ptr<MetadataScanBuilder>> MetadataScanBuilder::Make(
+ const MetadataTable& table) {
+ ICEBERG_PRECHECK(table.source_table() != nullptr, "Source table cannot be
null");
+ ICEBERG_PRECHECK(table.schema() != nullptr, "Metadata table schema cannot be
null");
+ return std::unique_ptr<MetadataScanBuilder>(new MetadataScanBuilder(table));
+}
+
MetadataScanBuilder& MetadataScanBuilder::Select(
const std::vector<std::string>& columns) {
- auto projected = table_->schema()->Select(columns, case_sensitive_);
+ auto projected = context_.schema->Select(columns, context_.case_sensitive);
if (projected) {
- projected_schema_ = std::move(projected).value();
+ context_.projected_schema = std::move(projected).value();
} else {
AddError(projected.error());
}
@@ -45,44 +135,61 @@ MetadataScanBuilder& MetadataScanBuilder::Select(
}
MetadataScanBuilder& MetadataScanBuilder::Project(std::shared_ptr<Schema>
schema) {
- projected_schema_ = std::move(schema);
+ context_.projected_schema = std::move(schema);
return *this;
}
MetadataScanBuilder& MetadataScanBuilder::CaseSensitive(bool value) {
- case_sensitive_ = value;
+ context_.case_sensitive = value;
return *this;
}
MetadataScanBuilder& MetadataScanBuilder::UseSnapshot(int64_t snapshot_id) {
ICEBERG_BUILDER_CHECK(
- std::holds_alternative<std::monostate>(snapshot_selection_.snapshot),
+ !context_.snapshot_id.has_value() &&
!context_.as_of_time_in_millis.has_value(),
"Cannot override snapshot, a snapshot or timestamp is already set");
- snapshot_selection_.snapshot = snapshot_id;
+ context_.snapshot_id = snapshot_id;
return *this;
}
MetadataScanBuilder& MetadataScanBuilder::UseRef(const std::string& ref) {
- ICEBERG_BUILDER_CHECK(snapshot_selection_.ref_name.empty(),
- "Cannot override ref, already set ref={}",
- snapshot_selection_.ref_name);
+ ICEBERG_BUILDER_CHECK(context_.ref_name.empty(),
+ "Cannot override ref, already set ref={}",
context_.ref_name);
ICEBERG_BUILDER_CHECK(!ref.empty(), "Ref name cannot be empty");
- snapshot_selection_.ref_name = ref;
+ context_.ref_name = ref;
return *this;
}
-MetadataScanBuilder& MetadataScanBuilder::AsOfTime(int64_t timestamp_millis) {
+MetadataScanBuilder& MetadataScanBuilder::AsOfTime(int64_t
as_of_time_in_millis) {
ICEBERG_BUILDER_CHECK(
- std::holds_alternative<std::monostate>(snapshot_selection_.snapshot),
+ !context_.snapshot_id.has_value() &&
!context_.as_of_time_in_millis.has_value(),
"Cannot override snapshot, a snapshot or timestamp is already set");
- snapshot_selection_.snapshot = TimePointMsFromUnixMs(timestamp_millis);
+ context_.as_of_time_in_millis = as_of_time_in_millis;
return *this;
}
-Result<ArrowArrayStream> MetadataScanBuilder::Build() {
+Result<std::unique_ptr<MetadataScan>> MetadataScanBuilder::Build() {
ICEBERG_RETURN_UNEXPECTED(CheckErrors());
- ICEBERG_PRECHECK(projected_schema_ != nullptr, "Projected schema cannot be
null");
- return table_->ScanSnapshot(snapshot_selection_, *projected_schema_);
+ ICEBERG_PRECHECK(context_.projected_schema != nullptr,
+ "Projected schema cannot be null");
+ return table_->MakeScan(context_);
+}
+
+MetadataScan::MetadataScan(internal::MetadataScanContext context)
+ : context_(std::move(context)) {}
+
+MetadataScan::~MetadataScan() = default;
+
+Result<std::vector<std::shared_ptr<MetadataScanTask>>>
MetadataScan::PlanTasks() const {
+ ICEBERG_ASSIGN_OR_RAISE(auto stream, PlanTasksStream());
+ return stream->ToVector();
+}
+
+Result<ArrowArrayStream> MetadataScan::ToArrowArrayStream() const {
+ ICEBERG_ASSIGN_OR_RAISE(auto tasks, PlanTasks());
+ ICEBERG_ASSIGN_OR_RAISE(auto reader, MetadataScanTaskReader::Make({}));
+ return MakeArrowArrayStream(std::make_unique<TaskArrowStreamSource>(
+ std::move(tasks), std::move(reader), context_.projected_schema));
}
} // namespace iceberg
diff --git a/src/iceberg/inspect/metadata_scan.h
b/src/iceberg/inspect/metadata_scan.h
index f487bce4..5df4cc03 100644
--- a/src/iceberg/inspect/metadata_scan.h
+++ b/src/iceberg/inspect/metadata_scan.h
@@ -20,12 +20,12 @@
#pragma once
/// \file iceberg/inspect/metadata_scan.h
-/// \brief Scan configuration for Iceberg metadata tables.
+/// \brief Configure metadata scans, plan reusable tasks, and read Arrow
batches.
#include <cstdint>
#include <memory>
+#include <optional>
#include <string>
-#include <variant>
#include <vector>
#include "iceberg/arrow_c_data.h"
@@ -33,104 +33,101 @@
#include "iceberg/result.h"
#include "iceberg/type_fwd.h"
#include "iceberg/util/error_collector.h"
-#include "iceberg/util/timepoint.h"
+#include "iceberg/util/stream.h"
namespace iceberg {
-/// \brief Snapshot selection parameters for a time-travel scan.
-struct SnapshotSelection {
- /// \brief Select the current snapshot, a snapshot ID, or an as-of timestamp.
- ///
- /// std::monostate selects the current snapshot.
- std::variant<std::monostate, int64_t, TimePointMs> snapshot;
+using MetadataScanTaskStream = Stream<std::shared_ptr<MetadataScanTask>>;
+using MetadataScanTaskStreamPtr = std::unique_ptr<MetadataScanTaskStream>;
- /// \brief Resolve the snapshot relative to this branch or tag.
- ///
- /// An empty string uses the main branch.
+namespace internal {
+
+/// \brief Captured metadata and logical settings shared by a builder and its
scans.
+struct MetadataScanContext {
+ MetadataTableKind kind;
+ std::shared_ptr<const TableMetadata> table_metadata;
+ std::shared_ptr<Schema> schema;
+ std::shared_ptr<Schema> projected_schema;
+ bool case_sensitive = true;
+ std::optional<int64_t> snapshot_id;
std::string ref_name;
+ std::optional<int64_t> as_of_time_in_millis;
};
-/// \brief Configure a scan over a metadata table.
-///
-/// Builders are created by MetadataTable::NewScan(), and the metadata table
must
-/// outlive the builder.
-///
-/// \code
-/// ICEBERG_ASSIGN_OR_RAISE(auto builder, snapshots_table->NewScan());
-/// ICEBERG_ASSIGN_OR_RAISE(auto stream,
builder->Select({"snapshot_id"}).Build());
-/// \endcode
+} // namespace internal
+
+/// \brief Configure a scan over captured metadata; the metadata table must
outlive it.
class ICEBERG_EXPORT MetadataScanBuilder : public ErrorCollector {
public:
~MetadataScanBuilder() override;
- /// \brief Select columns by name; nested columns use dot-separated paths.
- ///
- /// Columns are returned in schema order, and names that match no column are
- /// ignored. An empty list selects no columns; `*` selects all columns.
Names are
- /// resolved immediately, using the current CaseSensitive() setting.
Selecting a
- /// map's value returns the whole map; selecting only its key is an error.
+ /// \brief Capture the source metadata and schema of a metadata table.
+ static Result<std::unique_ptr<MetadataScanBuilder>> Make(const
MetadataTable& table);
+
+ /// \brief Select names using the current CaseSensitive() setting.
///
- /// \param columns Column names from the metadata table's schema.
+ /// Columns are returned in schema order; unknown names are ignored. An empty
+ /// list selects no columns and `*` selects all columns. Nested names use
dots;
+ /// selecting a map value selects the whole map, while selecting only its
key fails.
MetadataScanBuilder& Select(const std::vector<std::string>& columns);
- /// \brief Set the output schema to a projection of the metadata table
schema.
- ///
- /// Each field must select a whole column of the metadata table by field id,
with
- /// the same type and without tightening its nullability. Fields may be
reordered.
+ /// \brief Project whole columns by field ID, optionally reordering them.
///
- /// \param schema The output schema.
+ /// Types must match and nullable columns cannot become required.
MetadataScanBuilder& Project(std::shared_ptr<Schema> schema);
- /// \brief Use case-sensitive column names in Select(); enabled by default.
- ///
- /// \param value Whether column names are matched case-sensitively.
+ /// \brief Match column names case-sensitively; enabled by default.
MetadataScanBuilder& CaseSensitive(bool value);
- /// \brief Request this scan to use the given snapshot by ID.
- ///
- /// Only metadata tables that support time travel accept a snapshot; Build()
- /// returns NotSupported otherwise. The table resolves the snapshot when the
scan
- /// is built.
- ///
- /// \param snapshot_id A snapshot ID.
+ /// \brief Select a snapshot ID; Build rejects tables without time travel.
MetadataScanBuilder& UseSnapshot(int64_t snapshot_id);
- /// \brief Request this scan to resolve the snapshot on the given branch or
tag.
- ///
- /// Only metadata tables that support time travel accept a reference; Build()
- /// returns NotSupported otherwise. The table resolves the reference when
the scan
- /// is built.
- ///
- /// \param ref A branch or tag name.
+ /// \brief Select a branch or tag; Build rejects tables without time travel.
MetadataScanBuilder& UseRef(const std::string& ref);
- /// \brief Request this scan to use the most recent snapshot as of the given
time.
- ///
- /// The snapshot is resolved on the branch set by UseRef(), or main if no
branch
- /// is set. Only metadata tables that support time travel accept a timestamp;
- /// Build() returns NotSupported otherwise.
+ /// \brief Select the latest snapshot at or before this timestamp on the
selected ref.
///
- /// \param timestamp_millis A timestamp in milliseconds since the Unix epoch.
- MetadataScanBuilder& AsOfTime(int64_t timestamp_millis);
+ /// The timestamp is milliseconds since the Unix epoch. Build rejects tables
+ /// without time travel.
+ MetadataScanBuilder& AsOfTime(int64_t as_of_time_in_millis);
- /// \brief Validate the settings and scan the metadata table.
+ /// \brief Validate the settings and build a scan with its own copy of them.
///
- /// Each call returns a new stream with the output schema. The caller owns
the
- /// returned stream and must invoke its `release` callback when the stream
is no
- /// longer needed.
- ///
- /// \return An Arrow stream containing the selected columns, or an error.
- Result<ArrowArrayStream> Build();
+ /// Later builder changes and normal source table refreshes do not affect it.
+ Result<std::unique_ptr<MetadataScan>> Build();
private:
- friend class MetadataTable;
-
explicit MetadataScanBuilder(const MetadataTable& table);
const MetadataTable* table_;
- std::shared_ptr<Schema> projected_schema_;
- bool case_sensitive_ = true;
- SnapshotSelection snapshot_selection_;
+ internal::MetadataScanContext context_;
+};
+
+/// \brief A metadata scan that plans tasks or reads all of them as Arrow
batches.
+class ICEBERG_EXPORT MetadataScan {
+ public:
+ virtual ~MetadataScan();
+
+ /// \brief Return a fresh task stream; consuming it may report planning
errors.
+ virtual Result<MetadataScanTaskStreamPtr> PlanTasksStream() const = 0;
+
+ /// \brief Collect every task from PlanTasksStream(), preserving planning
order.
+ virtual Result<std::vector<std::shared_ptr<MetadataScanTask>>> PlanTasks()
const;
+
+ /// \brief Read every task in planning order as one Arrow stream.
+ ///
+ /// The stream owns its read state and can outlive this scan. Release the
stream
+ /// and each returned schema or array independently. To set batch size, open
the
+ /// planned tasks with MetadataScanTaskReader; each task retains its
projection.
+ virtual Result<ArrowArrayStream> ToArrowArrayStream() const;
+
+ protected:
+ explicit MetadataScan(internal::MetadataScanContext context);
+
+ const internal::MetadataScanContext& context() const { return context_; }
+
+ private:
+ const internal::MetadataScanContext context_;
};
} // namespace iceberg
diff --git a/src/iceberg/inspect/metadata_scan_task.cc
b/src/iceberg/inspect/metadata_scan_task.cc
new file mode 100644
index 00000000..afb3eb10
--- /dev/null
+++ b/src/iceberg/inspect/metadata_scan_task.cc
@@ -0,0 +1,51 @@
+/*
+ * 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/metadata_scan_task.h"
+
+#include <utility>
+
+#include "iceberg/inspect/metadata_table.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+// StaticMetadataTask
+
+Result<std::shared_ptr<StaticMetadataTask>> StaticMetadataTask::Make(
+ MetadataTableKind table_kind, std::shared_ptr<const TableMetadata>
metadata,
+ std::shared_ptr<Schema> projected_schema) {
+ ICEBERG_PRECHECK(metadata != nullptr, "Table metadata cannot be null");
+ ICEBERG_PRECHECK(projected_schema != nullptr, "Projected schema cannot be
null");
+ if (table_kind != MetadataTableKind::kSnapshots &&
+ table_kind != MetadataTableKind::kHistory) {
+ return NotImplemented("Static metadata table kind is not implemented");
+ }
+ return std::shared_ptr<StaticMetadataTask>(new StaticMetadataTask(
+ table_kind, std::move(metadata), std::move(projected_schema)));
+}
+
+StaticMetadataTask::StaticMetadataTask(MetadataTableKind table_kind,
+ std::shared_ptr<const TableMetadata>
metadata,
+ std::shared_ptr<Schema>
projected_schema)
+ : table_kind_(table_kind),
+ metadata_(std::move(metadata)),
+ projected_schema_(std::move(projected_schema)) {}
+
+} // namespace iceberg
diff --git a/src/iceberg/inspect/metadata_scan_task.h
b/src/iceberg/inspect/metadata_scan_task.h
new file mode 100644
index 00000000..b3511e94
--- /dev/null
+++ b/src/iceberg/inspect/metadata_scan_task.h
@@ -0,0 +1,88 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <memory>
+
+#include "iceberg/iceberg_export.h"
+#include "iceberg/result.h"
+#include "iceberg/type_fwd.h"
+
+namespace iceberg {
+
+/// \brief Base class for metadata table scan tasks.
+///
+/// Metadata tables expose table metadata (snapshots, history, etc.) as
queryable
+/// Arrow tables. A task is a unit of work opened by MetadataScanTaskReader.
+class ICEBERG_EXPORT MetadataScanTask {
+ public:
+ /// \brief Task category, determines how the reader opens the task.
+ enum class TaskKind {
+ /// StaticMetadataTask reads in-memory table metadata (snapshots, history).
+ kStatic,
+ };
+
+ virtual ~MetadataScanTask() = default;
+
+ /// \brief Return how this task is read.
+ virtual TaskKind task_kind() const = 0;
+
+ /// \brief Return the metadata table whose rows this task produces.
+ virtual MetadataTableKind table_kind() const = 0;
+};
+
+/// \brief Task for static metadata tables (snapshots, history).
+///
+/// Holds the metadata captured for a table scan; readers produce Arrow batches
+/// on demand. The metadata and its nested objects must not be modified while
+/// this task or its streams are in use.
+class ICEBERG_EXPORT StaticMetadataTask : public MetadataScanTask {
+ public:
+ /// \brief Create a static metadata task.
+ ///
+ /// \param table_kind The metadata table kind (kSnapshots or kHistory)
+ /// \param metadata The table metadata to scan
+ /// \param projected_schema The planned output schema
+ /// \return A task, or an error for null metadata or an unsupported table
kind.
+ static Result<std::shared_ptr<StaticMetadataTask>> Make(
+ MetadataTableKind table_kind, std::shared_ptr<const TableMetadata>
metadata,
+ std::shared_ptr<Schema> projected_schema);
+
+ TaskKind task_kind() const override { return TaskKind::kStatic; }
+
+ MetadataTableKind table_kind() const override { return table_kind_; }
+
+ /// \brief Return the captured metadata and its shared ownership.
+ const std::shared_ptr<const TableMetadata>& metadata() const { return
metadata_; }
+
+ /// \brief Return the output schema captured when this task was planned.
+ const std::shared_ptr<Schema>& projected_schema() const { return
projected_schema_; }
+
+ private:
+ StaticMetadataTask(MetadataTableKind table_kind,
+ std::shared_ptr<const TableMetadata> metadata,
+ std::shared_ptr<Schema> projected_schema);
+
+ const MetadataTableKind table_kind_;
+ const std::shared_ptr<const TableMetadata> metadata_;
+ const std::shared_ptr<Schema> projected_schema_;
+};
+
+} // namespace iceberg
diff --git a/src/iceberg/inspect/metadata_scan_task_reader.cc
b/src/iceberg/inspect/metadata_scan_task_reader.cc
new file mode 100644
index 00000000..1ce5b3c1
--- /dev/null
+++ b/src/iceberg/inspect/metadata_scan_task_reader.cc
@@ -0,0 +1,68 @@
+/*
+ * 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/metadata_scan_task_reader.h"
+
+#include <memory>
+#include <utility>
+
+#include "iceberg/inspect/history_table.h"
+#include "iceberg/inspect/metadata_scan_task.h"
+#include "iceberg/inspect/metadata_table.h"
+#include "iceberg/inspect/snapshots_table.h"
+#include "iceberg/util/checked_cast.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+MetadataScanTaskReader::MetadataScanTaskReader(Options options)
+ : options_(std::move(options)) {}
+
+MetadataScanTaskReader::~MetadataScanTaskReader() = default;
+
+Result<std::unique_ptr<MetadataScanTaskReader>> MetadataScanTaskReader::Make(
+ Options options) {
+ ICEBERG_PRECHECK(options.batch_size > 0, "batch size must be positive");
+ return std::unique_ptr<MetadataScanTaskReader>(
+ new MetadataScanTaskReader(std::move(options)));
+}
+
+Result<ArrowArrayStream> MetadataScanTaskReader::Open(
+ const MetadataScanTask& task) const {
+ switch (task.task_kind()) {
+ case MetadataScanTask::TaskKind::kStatic: {
+ const auto& static_task = internal::checked_cast<const
StaticMetadataTask&>(task);
+ switch (static_task.table_kind()) {
+ case MetadataTableKind::kSnapshots:
+ return SnapshotsTable::ReadRows(static_task.metadata(),
+ static_task.projected_schema(),
+ options_.batch_size);
+ case MetadataTableKind::kHistory:
+ return HistoryTable::ReadRows(static_task.metadata(),
+ static_task.projected_schema(),
+ options_.batch_size);
+ default:
+ return NotImplemented("Static metadata table kind is not
implemented");
+ }
+ }
+ }
+ return NotImplemented("Metadata scan task kind is not implemented");
+}
+
+} // namespace iceberg
diff --git a/src/iceberg/inspect/metadata_scan_task_reader.h
b/src/iceberg/inspect/metadata_scan_task_reader.h
new file mode 100644
index 00000000..8003276b
--- /dev/null
+++ b/src/iceberg/inspect/metadata_scan_task_reader.h
@@ -0,0 +1,61 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <cstdint>
+#include <memory>
+
+#include "iceberg/arrow_c_data.h"
+#include "iceberg/iceberg_export.h"
+#include "iceberg/result.h"
+#include "iceberg/type_fwd.h"
+
+namespace iceberg {
+
+/// \brief Opens metadata scan tasks as Arrow streams.
+class ICEBERG_EXPORT MetadataScanTaskReader {
+ public:
+ /// \brief Options shared by all tasks opened by this reader.
+ struct Options {
+ /// Positive maximum number of rows per Arrow batch.
+ int64_t batch_size = 1024;
+ };
+
+ /// \brief Create a reusable task reader from shared read options.
+ static Result<std::unique_ptr<MetadataScanTaskReader>> Make(Options options);
+
+ ~MetadataScanTaskReader();
+
+ /// \brief Open a task and return an Arrow C stream for its projected rows.
+ ///
+ /// Batches are built on demand. The stream retains its input data and can
+ /// outlive this reader and the task; each call starts an independent read.
+ Result<ArrowArrayStream> Open(const MetadataScanTask& task) const;
+
+ MetadataScanTaskReader(const MetadataScanTaskReader&) = delete;
+ MetadataScanTaskReader& operator=(const MetadataScanTaskReader&) = delete;
+
+ private:
+ explicit MetadataScanTaskReader(Options options);
+
+ const Options options_;
+};
+
+} // namespace iceberg
diff --git a/src/iceberg/inspect/metadata_table_columns_internal.h
b/src/iceberg/inspect/metadata_scan_util_internal.h
similarity index 56%
rename from src/iceberg/inspect/metadata_table_columns_internal.h
rename to src/iceberg/inspect/metadata_scan_util_internal.h
index 596ffa2f..5cd2058f 100644
--- a/src/iceberg/inspect/metadata_table_columns_internal.h
+++ b/src/iceberg/inspect/metadata_scan_util_internal.h
@@ -19,20 +19,25 @@
#pragma once
-/// \file iceberg/inspect/metadata_table_columns_internal.h
-/// \brief Build only the selected columns of metadata table rows.
+/// \file iceberg/inspect/metadata_scan_util_internal.h
+/// \brief Read in-memory metadata as Arrow batches produced on demand.
#include <algorithm>
-#include <cstddef>
#include <cstdint>
+#include <memory>
+#include <optional>
#include <span>
+#include <type_traits>
+#include <utility>
#include <vector>
#include "iceberg/arrow_c_data.h"
+#include "iceberg/arrow_c_data_util_internal.h"
#include "iceberg/arrow_row_builder_internal.h"
#include "iceberg/result.h"
#include "iceberg/schema.h"
#include "iceberg/schema_field.h"
+#include "iceberg/schema_internal.h"
#include "iceberg/type.h"
#include "iceberg/util/macros.h"
@@ -100,4 +105,80 @@ Status AppendRow(ArrowRowBuilder& builder,
return builder.FinishRow();
}
+/// \brief Build batches lazily, retaining the row producer and its input
owners.
+template <typename Row, typename GetRow>
+class MetadataBatchSource {
+ public:
+ MetadataBatchSource(std::shared_ptr<iceberg::Schema> schema,
+ std::vector<AppendColumnFn<Row>> append_fns, size_t
num_rows,
+ int64_t batch_size, GetRow get_row)
+ : schema_(std::move(schema)),
+ append_fns_(std::move(append_fns)),
+ num_rows_(num_rows),
+ batch_size_(static_cast<size_t>(batch_size)),
+ get_row_(std::move(get_row)) {}
+
+ Status Close() { return {}; }
+
+ Result<ArrowSchema> Schema() {
+ ArrowSchema schema{};
+ ICEBERG_RETURN_UNEXPECTED(ToArrowSchema(*schema_, &schema));
+ return schema;
+ }
+
+ Result<std::optional<ArrowArray>> Next() {
+ if (error_) {
+ return unexpected(*error_);
+ }
+ auto result = NextBatch();
+ if (!result) {
+ error_ = result.error();
+ }
+ return result;
+ }
+
+ private:
+ Result<std::optional<ArrowArray>> NextBatch() {
+ if (index_ == num_rows_) {
+ return std::nullopt;
+ }
+ const size_t end = index_ + std::min(batch_size_, num_rows_ - index_);
+ ICEBERG_ASSIGN_OR_RAISE(auto builder, ArrowRowBuilder::Make(*schema_));
+ while (index_ < end) {
+ ICEBERG_RETURN_UNEXPECTED(AppendRow<Row>(builder, append_fns_,
get_row_(index_)));
+ ++index_;
+ }
+ ICEBERG_ASSIGN_OR_RAISE(auto batch, std::move(builder).Finish());
+ return batch;
+ }
+
+ const std::shared_ptr<iceberg::Schema> schema_;
+ const std::vector<AppendColumnFn<Row>> append_fns_;
+ const size_t num_rows_;
+ const size_t batch_size_;
+ GetRow get_row_;
+ size_t index_ = 0;
+ std::optional<Error> error_;
+};
+
+/// \brief Read only projected columns, constructing at most batch_size rows
per batch.
+///
+/// get_row must retain shared ownership of its inputs. Each stream has
independent
+/// read state, and rows are accessed only when a batch is requested.
+template <typename Row, typename GetRow>
+Result<ArrowArrayStream> MakeMetadataBatchStream(
+ const std::shared_ptr<Schema>& canonical_schema,
+ const std::shared_ptr<Schema>& projected_schema,
+ std::span<const MetadataTableColumn<Row>> columns, size_t num_rows,
+ int64_t batch_size, GetRow&& get_row) {
+ ICEBERG_PRECHECK(batch_size > 0, "Batch size must be positive");
+ ICEBERG_ASSIGN_OR_RAISE(
+ auto append_fns,
+ SelectAppendFns<Row>(*canonical_schema, *projected_schema, columns));
+ using Source = MetadataBatchSource<Row, std::decay_t<GetRow>>;
+ return MakeArrowArrayStream(
+ std::make_unique<Source>(projected_schema, std::move(append_fns),
num_rows,
+ batch_size, std::forward<GetRow>(get_row)));
+}
+
} // namespace iceberg::internal
diff --git a/src/iceberg/inspect/metadata_table.cc
b/src/iceberg/inspect/metadata_table.cc
index 3e21688f..6bd17644 100644
--- a/src/iceberg/inspect/metadata_table.cc
+++ b/src/iceberg/inspect/metadata_table.cc
@@ -21,46 +21,37 @@
#include <memory>
#include <utility>
-#include <variant>
+#include "iceberg/inspect/history_table.h"
#include "iceberg/inspect/metadata_scan.h"
+#include "iceberg/inspect/snapshots_table.h"
namespace iceberg {
-MetadataTable::MetadataTable(std::shared_ptr<Table> source_table)
+MetadataTable::MetadataTable(std::shared_ptr<const Table> source_table)
: source_table_(std::move(source_table)) {}
MetadataTable::~MetadataTable() = default;
-bool MetadataTable::supports_time_travel() const noexcept { return false; }
+Result<std::unique_ptr<MetadataTable>> MetadataTable::Make(
+ std::shared_ptr<const Table> table, MetadataTableKind kind) {
+ switch (kind) {
+ case MetadataTableKind::kSnapshots:
+ return SnapshotsTable::Make(std::move(table));
+ case MetadataTableKind::kHistory:
+ return HistoryTable::Make(std::move(table));
+ default:
+ break;
+ }
+ return NotImplemented("Metadata table kind is not implemented");
+}
-const std::shared_ptr<Table>& MetadataTable::source_table() const {
+const std::shared_ptr<const Table>& MetadataTable::source_table() const {
return source_table_;
}
Result<std::unique_ptr<MetadataScanBuilder>> MetadataTable::NewScan() const {
- return std::unique_ptr<MetadataScanBuilder>(new MetadataScanBuilder(*this));
-}
-
-Result<ArrowArrayStream> MetadataTable::ScanSnapshot(
- const SnapshotSelection& snapshot_selection, const Schema&
projected_schema) const {
- if (!std::holds_alternative<std::monostate>(snapshot_selection.snapshot) ||
- !snapshot_selection.ref_name.empty()) {
- return NotSupported("Metadata table does not support time travel");
- }
- return ScanProjection(projected_schema);
-}
-
-TimeTravelMetadataTable::TimeTravelMetadataTable(std::shared_ptr<Table>
source_table)
- : MetadataTable(std::move(source_table)) {}
-
-TimeTravelMetadataTable::~TimeTravelMetadataTable() = default;
-
-bool TimeTravelMetadataTable::supports_time_travel() const noexcept { return
true; }
-
-Result<ArrowArrayStream> TimeTravelMetadataTable::ScanProjection(
- const Schema& projected_schema) const {
- return ScanSnapshot(SnapshotSelection{}, projected_schema);
+ return MetadataScanBuilder::Make(*this);
}
} // namespace iceberg
diff --git a/src/iceberg/inspect/metadata_table.h
b/src/iceberg/inspect/metadata_table.h
index 7717ce58..83f44f5c 100644
--- a/src/iceberg/inspect/metadata_table.h
+++ b/src/iceberg/inspect/metadata_table.h
@@ -20,40 +20,56 @@
#pragma once
/// \file iceberg/inspect/metadata_table.h
-/// \brief Base APIs for inspecting Iceberg metadata tables.
+/// \brief Metadata table factories, schemas, and scan entry points.
#include <concepts>
+#include <cstdint>
#include <memory>
#include <utility>
-#include "iceberg/arrow_c_data.h"
#include "iceberg/iceberg_export.h"
-#include "iceberg/inspect/metadata_scan.h"
#include "iceberg/result.h"
#include "iceberg/type_fwd.h"
namespace iceberg {
+namespace internal {
+struct MetadataScanContext;
+} // namespace internal
+
+/// \brief Iceberg metadata tables exposed by Table::Inspect().
+enum class MetadataTableKind : uint8_t {
+ kEntries,
+ kFiles,
+ kDataFiles,
+ kDeleteFiles,
+ kHistory,
+ kMetadataLogEntries,
+ kSnapshots,
+ kRefs,
+ kManifests,
+ kPartitions,
+ kPositionDeletes,
+ kAllEntries,
+ kAllFiles,
+ kAllDataFiles,
+ kAllDeleteFiles,
+ kAllManifests,
+};
-/// \brief Base interface for an Iceberg metadata table.
+/// \brief A metadata table backed by a source Iceberg table.
class ICEBERG_EXPORT MetadataTable {
public:
- /// \brief Supported metadata table kinds.
- enum class Kind {
- kSnapshots,
- kHistory,
- };
+ using Kind = MetadataTableKind;
- /// \brief Maximum number of rows emitted in each Arrow batch.
- static constexpr int64_t kBatchSize = 1024;
+ /// \brief Create a metadata table; unsupported kinds return NotImplemented.
+ static Result<std::unique_ptr<MetadataTable>> Make(std::shared_ptr<const
Table> table,
+ Kind kind);
- /// \brief Create a metadata table of the requested concrete type.
- ///
- /// \tparam MetadataTableType Concrete class derived from MetadataTable.
- /// \param table Source table whose metadata will be exposed.
- /// \return The constructed metadata table, or an error.
+ /// \brief Create a specific metadata table from its concrete class.
template <typename MetadataTableType>
requires std::derived_from<MetadataTableType, MetadataTable>
- static Result<std::unique_ptr<MetadataTableType>>
Make(std::shared_ptr<Table> table) {
+ static Result<std::unique_ptr<MetadataTableType>> Make(
+ std::shared_ptr<const Table> table) {
return MetadataTableType::Make(std::move(table));
}
@@ -66,70 +82,25 @@ class ICEBERG_EXPORT MetadataTable {
virtual const std::shared_ptr<Schema>& schema() const = 0;
/// \brief Return the source table whose metadata is exposed.
- const std::shared_ptr<Table>& source_table() const;
+ const std::shared_ptr<const Table>& source_table() const;
- /// \brief Return whether this metadata table supports time travel.
- virtual bool supports_time_travel() const noexcept;
-
- /// \brief Create a builder for configuring a metadata table scan.
+ /// \brief Capture the source metadata and create a scan builder.
///
- /// A builder with no options scans all columns of the current snapshot on
the main
- /// branch. This table must outlive the builder.
+ /// This metadata table must outlive the builder. Built scans retain their
own
+ /// inputs and can outlive both tables.
virtual Result<std::unique_ptr<MetadataScanBuilder>> NewScan() const;
protected:
- explicit MetadataTable(std::shared_ptr<Table> source_table);
-
- /// \brief Implement a scan that returns the columns of a projected schema.
- ///
- /// \param projected_schema Projection of schema() with the columns to
return. It
- /// is only valid during this call.
- virtual Result<ArrowArrayStream> ScanProjection(
- const Schema& projected_schema) const = 0;
+ explicit MetadataTable(std::shared_ptr<const Table> source_table);
- /// \brief Implement a scan for the requested snapshot selection.
- ///
- /// The default implementation scans the current snapshot on the main branch
with
- /// ScanProjection() and returns NotSupported for any other selection.
- /// TimeTravelMetadataTable requires an implementation.
- ///
- /// \param snapshot_selection Snapshot ID, timestamp, and optional ref
selection.
- /// \param projected_schema Projection of schema() with the columns to
return. It
- /// is only valid during this call.
- virtual Result<ArrowArrayStream> ScanSnapshot(
- const SnapshotSelection& snapshot_selection, const Schema&
projected_schema) const;
+ /// \brief Plan a scan using the captured metadata, projection, and snapshot
selection.
+ virtual Result<std::unique_ptr<MetadataScan>> MakeScan(
+ internal::MetadataScanContext context) const = 0;
private:
friend class MetadataScanBuilder;
- std::shared_ptr<Table> source_table_;
-};
-
-/// \brief Base interface for metadata tables that support time travel.
-///
-/// Scans of these tables accept MetadataScanBuilder::UseSnapshot(), UseRef(),
and
-/// AsOfTime(), which MetadataScanBuilder::Build() rejects for other tables.
-class ICEBERG_EXPORT TimeTravelMetadataTable : public MetadataTable {
- public:
- ~TimeTravelMetadataTable() override;
-
- /// \brief Return true because this interface supports time travel.
- bool supports_time_travel() const noexcept final;
-
- protected:
- explicit TimeTravelMetadataTable(std::shared_ptr<Table> source_table);
-
- /// \brief Scan the current snapshot on the main branch.
- Result<ArrowArrayStream> ScanProjection(const Schema& projected_schema)
const final;
-
- /// \brief Implement a scan for the requested snapshot selection.
- ///
- /// \param snapshot_selection Snapshot ID, timestamp, and optional ref
selection.
- /// \param projected_schema Projection of schema() with the columns to
return. It
- /// is only valid during this call.
- Result<ArrowArrayStream> ScanSnapshot(const SnapshotSelection&
snapshot_selection,
- const Schema& projected_schema) const
override =
- 0;
+ const std::shared_ptr<const Table> source_table_;
};
} // namespace iceberg
diff --git a/src/iceberg/inspect/snapshots_table.cc
b/src/iceberg/inspect/snapshots_table.cc
index 0dede12c..7eb204b4 100644
--- a/src/iceberg/inspect/snapshots_table.cc
+++ b/src/iceberg/inspect/snapshots_table.cc
@@ -20,22 +20,15 @@
#include "iceberg/inspect/snapshots_table.h"
#include <array>
-#include <chrono>
-#include <cstddef>
#include <memory>
-#include <optional>
#include <utility>
#include <vector>
-#include <nanoarrow/nanoarrow.h>
-
-#include "iceberg/arrow_c_data_util_internal.h"
#include "iceberg/arrow_row_builder_internal.h"
-#include "iceberg/inspect/metadata_table_columns_internal.h"
-#include "iceberg/nanoarrow_status_internal.h"
+#include "iceberg/inspect/metadata_scan_task.h"
+#include "iceberg/inspect/metadata_scan_util_internal.h"
#include "iceberg/schema.h"
#include "iceberg/schema_field.h"
-#include "iceberg/schema_internal.h"
#include "iceberg/snapshot.h"
#include "iceberg/table.h"
#include "iceberg/table_metadata.h"
@@ -45,165 +38,127 @@
namespace iceberg {
namespace {
-Status AppendCommittedAt(ArrowArray* column, const Snapshot& snapshot) {
- return AppendInt(column,
std::chrono::duration_cast<std::chrono::microseconds>(
- snapshot.timestamp_ms.time_since_epoch())
- .count());
-}
-
-Status AppendSnapshotId(ArrowArray* column, const Snapshot& snapshot) {
- return AppendInt(column, snapshot.snapshot_id);
-}
-
-Status AppendParentId(ArrowArray* column, const Snapshot& snapshot) {
- if (snapshot.parent_snapshot_id.has_value()) {
- return AppendInt(column, *snapshot.parent_snapshot_id);
- }
- return AppendNull(column);
-}
-
-Status AppendOperation(ArrowArray* column, const Snapshot& snapshot) {
- auto operation = snapshot.Operation();
- if (operation.has_value()) {
- return AppendString(column, *operation);
- }
- return AppendNull(column);
-}
-
-Status AppendManifestList(ArrowArray* column, const Snapshot& snapshot) {
- return AppendString(column, snapshot.manifest_list);
-}
-
-Status AppendSummary(ArrowArray* column, const Snapshot& snapshot) {
- if (snapshot.summary.empty()) {
- return AppendNull(column);
- }
- if (snapshot.summary.contains(SnapshotSummaryFields::kOperation)) {
- auto summary = snapshot.summary;
- summary.erase(SnapshotSummaryFields::kOperation);
- return AppendStringMap(column, summary);
- }
- return AppendStringMap(column, snapshot.summary);
-}
-
-constexpr std::array<internal::MetadataTableColumn<Snapshot>, 6>
kSnapshotColumns{{
- {.field_id = 1, .append = AppendCommittedAt},
- {.field_id = 2, .append = AppendSnapshotId},
- {.field_id = 3, .append = AppendParentId},
- {.field_id = 4, .append = AppendOperation},
- {.field_id = 5, .append = AppendManifestList},
- {.field_id = 6, .append = AppendSummary},
-}};
-
-class SnapshotsTableStream {
- public:
- static Result<std::unique_ptr<SnapshotsTableStream>> Make(
- std::shared_ptr<const TableMetadata> metadata,
- const iceberg::Schema& projected_schema,
- std::vector<internal::AppendColumnFn<Snapshot>> append_fns) {
- ArrowSchema arrow_schema{};
- ICEBERG_RETURN_UNEXPECTED(ToArrowSchema(projected_schema, &arrow_schema));
- return std::unique_ptr<SnapshotsTableStream>(new SnapshotsTableStream(
- std::move(metadata), std::move(append_fns), std::move(arrow_schema)));
- }
-
- ~SnapshotsTableStream() = default;
-
- Status Close() {
- metadata_.reset();
- if (arrow_schema_.release != nullptr) {
- ArrowSchemaRelease(&arrow_schema_);
- }
- return {};
- }
-
- Result<std::optional<ArrowArray>> Next() {
- if (metadata_ == nullptr) [[unlikely]] {
- return InvalidArgument("Cannot read from a closed snapshots table
stream");
- }
- const auto& snapshots = metadata_->snapshots;
- if (next_snapshot_ == snapshots.size()) {
- return std::nullopt;
- }
-
- ICEBERG_ASSIGN_OR_RAISE(auto builder,
ArrowRowBuilder::Make(&arrow_schema_));
- while (next_snapshot_ < snapshots.size() &&
- builder.num_rows() < MetadataTable::kBatchSize) {
- const auto& snapshot = snapshots[next_snapshot_++];
- if (snapshot == nullptr) [[unlikely]] {
- continue;
- }
- ICEBERG_RETURN_UNEXPECTED(
- internal::AppendRow<Snapshot>(builder, append_fns_, *snapshot));
- }
- if (builder.num_rows() == 0) {
- return std::nullopt;
- }
-
- ICEBERG_ASSIGN_OR_RAISE(auto array, std::move(builder).Finish());
- return array;
- }
-
- Result<ArrowSchema> Schema() {
- if (arrow_schema_.release == nullptr) [[unlikely]] {
- return InvalidArgument("Cannot read schema from a closed snapshots table
stream");
- }
- ArrowSchema schema_copy{};
- ICEBERG_NANOARROW_RETURN_UNEXPECTED(
- ArrowSchemaDeepCopy(&arrow_schema_, &schema_copy));
- return schema_copy;
- }
-
- private:
- SnapshotsTableStream(std::shared_ptr<const TableMetadata> metadata,
- std::vector<internal::AppendColumnFn<Snapshot>>
append_fns,
- ArrowSchema arrow_schema)
- : metadata_(std::move(metadata)),
- append_fns_(std::move(append_fns)),
- arrow_schema_(std::move(arrow_schema)) {}
-
- std::shared_ptr<const TableMetadata> metadata_;
- std::vector<internal::AppendColumnFn<Snapshot>> append_fns_;
- ArrowSchema arrow_schema_{};
- size_t next_snapshot_ = 0;
-};
+constexpr std::array<internal::MetadataTableColumn<Snapshot>, 6>
kSnapshotColumns = {
+ {{.field_id = 1,
+ .append =
+ [](ArrowArray* column, const Snapshot& row) {
+ return AppendInt(column, UnixMsFromTimePointMs(row.timestamp_ms) *
1000);
+ }},
+ {.field_id = 2,
+ .append = [](ArrowArray* column,
+ const Snapshot& row) { return AppendInt(column,
row.snapshot_id); }},
+ {.field_id = 3,
+ .append =
+ [](ArrowArray* column, const Snapshot& row) {
+ return row.parent_snapshot_id ? AppendInt(column,
*row.parent_snapshot_id)
+ : AppendNull(column);
+ }},
+ {.field_id = 4,
+ .append =
+ [](ArrowArray* column, const Snapshot& row) {
+ auto operation = row.Operation();
+ return operation ? AppendString(column, *operation) :
AppendNull(column);
+ }},
+ {.field_id = 5,
+ .append =
+ [](ArrowArray* column, const Snapshot& row) {
+ return row.manifest_list.empty() ? AppendNull(column)
+ : AppendString(column,
row.manifest_list);
+ }},
+ {.field_id = 6, .append = [](ArrowArray* column, const Snapshot& row) {
+ if (row.summary.empty()) {
+ return AppendNull(column);
+ }
+ auto summary = row.summary;
+ summary.erase(SnapshotSummaryFields::kOperation);
+ return AppendStringMap(column, summary);
+ }}}};
} // namespace
-SnapshotsTable::SnapshotsTable(std::shared_ptr<Table> table)
+// SnapshotsTable implementation
+SnapshotsTable::SnapshotsTable(std::shared_ptr<const Table> table)
: MetadataTable(std::move(table)) {}
SnapshotsTable::~SnapshotsTable() = default;
const std::shared_ptr<Schema>& SnapshotsTable::schema() const {
+ return CanonicalSchema();
+}
+
+const std::shared_ptr<Schema>& SnapshotsTable::CanonicalSchema() {
static const auto schema = std::make_shared<Schema>(std::vector<SchemaField>{
SchemaField::MakeRequired(1, "committed_at", timestamp_tz()),
SchemaField::MakeRequired(2, "snapshot_id", int64()),
SchemaField::MakeOptional(3, "parent_id", int64()),
SchemaField::MakeOptional(4, "operation", string()),
SchemaField::MakeOptional(5, "manifest_list", string()),
- SchemaField::MakeOptional(6, "summary",
- std::make_shared<iceberg::MapType>(
- SchemaField::MakeRequired(7, "key",
string()),
+ SchemaField::MakeOptional(
+ 6, "summary",
+ std::make_shared<MapType>(SchemaField::MakeRequired(7, "key",
string()),
SchemaField::MakeRequired(8, "value",
string())))});
return schema;
}
Result<std::unique_ptr<SnapshotsTable>> SnapshotsTable::Make(
- std::shared_ptr<Table> table) {
+ std::shared_ptr<const Table> table) {
ICEBERG_PRECHECK(table != nullptr, "Table cannot be null");
return std::unique_ptr<SnapshotsTable>(new SnapshotsTable(std::move(table)));
}
-Result<ArrowArrayStream> SnapshotsTable::ScanProjection(
- const Schema& projected_schema) const {
- ICEBERG_ASSIGN_OR_RAISE(
- auto append_fns,
- internal::SelectAppendFns<Snapshot>(*schema(), projected_schema,
kSnapshotColumns));
- ICEBERG_ASSIGN_OR_RAISE(
- auto stream, SnapshotsTableStream::Make(source_table()->metadata(),
- projected_schema,
std::move(append_fns)));
- return MakeArrowArrayStream(std::move(stream));
+Result<std::unique_ptr<MetadataScan>> SnapshotsTable::MakeScan(
+ internal::MetadataScanContext context) const {
+ return SnapshotsScan::Make(std::move(context));
+}
+
+Result<ArrowArrayStream> SnapshotsTable::ReadRows(
+ std::shared_ptr<const TableMetadata> metadata,
+ const std::shared_ptr<Schema>& projected_schema, int64_t batch_size) {
+ std::vector<size_t> snapshot_indices;
+ for (size_t i = 0; i < metadata->snapshots.size(); ++i) {
+ if (metadata->snapshots[i]) {
+ snapshot_indices.push_back(i);
+ }
+ }
+ const size_t num_rows = snapshot_indices.size();
+ return internal::MakeMetadataBatchStream<Snapshot>(
+ CanonicalSchema(), projected_schema, kSnapshotColumns, num_rows,
batch_size,
+ [metadata = std::move(metadata),
+ snapshot_indices = std::move(snapshot_indices)](size_t index) -> const
Snapshot& {
+ return *metadata->snapshots[snapshot_indices[index]];
+ });
+}
+
+// SnapshotsScan implementation
+SnapshotsScan::SnapshotsScan(internal::MetadataScanContext context,
+ std::shared_ptr<MetadataScanTask> task)
+ : MetadataScan(std::move(context)), task_(std::move(task)) {}
+
+SnapshotsScan::~SnapshotsScan() = default;
+
+Result<std::unique_ptr<SnapshotsScan>> SnapshotsScan::Make(
+ internal::MetadataScanContext context) {
+ ICEBERG_PRECHECK(context.kind == MetadataTableKind::kSnapshots,
+ "Unexpected metadata table kind for snapshots scan");
+ if (context.snapshot_id.has_value() || !context.ref_name.empty() ||
+ context.as_of_time_in_millis.has_value()) {
+ return NotSupported("Metadata table does not support time travel");
+ }
+ ICEBERG_PRECHECK(context.table_metadata != nullptr, "Table metadata cannot
be null");
+ ICEBERG_PRECHECK(context.schema != nullptr, "Metadata table schema cannot be
null");
+ ICEBERG_PRECHECK(context.projected_schema != nullptr,
+ "Projected schema cannot be null");
+ ICEBERG_RETURN_UNEXPECTED(internal::SelectAppendFns<Snapshot>(
+ *SnapshotsTable::CanonicalSchema(), *context.projected_schema,
kSnapshotColumns));
+ ICEBERG_ASSIGN_OR_RAISE(auto task,
+ StaticMetadataTask::Make(context.kind,
context.table_metadata,
+ context.projected_schema));
+ return std::unique_ptr<SnapshotsScan>(
+ new SnapshotsScan(std::move(context), std::move(task)));
+}
+
+Result<MetadataScanTaskStreamPtr> SnapshotsScan::PlanTasksStream() const {
+ return MetadataScanTaskStream::FromVector({task_});
}
} // namespace iceberg
diff --git a/src/iceberg/inspect/snapshots_table.h
b/src/iceberg/inspect/snapshots_table.h
index ae1434a5..4bfaa554 100644
--- a/src/iceberg/inspect/snapshots_table.h
+++ b/src/iceberg/inspect/snapshots_table.h
@@ -22,9 +22,12 @@
/// \file iceberg/inspect/snapshots_table.h
/// \brief Define the snapshots metadata table.
+#include <cstdint>
#include <memory>
+#include "iceberg/arrow_c_data.h"
#include "iceberg/iceberg_export.h"
+#include "iceberg/inspect/metadata_scan.h"
#include "iceberg/inspect/metadata_table.h"
#include "iceberg/result.h"
#include "iceberg/type_fwd.h"
@@ -34,22 +37,49 @@ namespace iceberg {
/// \brief Snapshots metadata table.
class ICEBERG_EXPORT SnapshotsTable : public MetadataTable {
public:
- static Result<std::unique_ptr<SnapshotsTable>> Make(std::shared_ptr<Table>
table);
+ static Result<std::unique_ptr<SnapshotsTable>> Make(std::shared_ptr<const
Table> table);
+
+ /// \brief Return the canonical schema of the snapshots metadata table.
+ static const std::shared_ptr<Schema>& CanonicalSchema();
~SnapshotsTable() override;
- Kind kind() const noexcept override { return Kind::kSnapshots; }
+ MetadataTableKind kind() const noexcept override {
+ return MetadataTableKind::kSnapshots;
+ }
const std::shared_ptr<Schema>& schema() const override;
protected:
- /// \brief Scan all snapshots as rows.
- ///
- /// The snapshots table always returns every known snapshot.
- Result<ArrowArrayStream> ScanProjection(const Schema& projected_schema)
const override;
+ Result<std::unique_ptr<MetadataScan>> MakeScan(
+ internal::MetadataScanContext context) const override;
private:
- explicit SnapshotsTable(std::shared_ptr<Table> table);
+ friend class MetadataScanTaskReader;
+
+ static Result<ArrowArrayStream> ReadRows(
+ std::shared_ptr<const TableMetadata> metadata,
+ const std::shared_ptr<Schema>& projected_schema, int64_t batch_size);
+
+ explicit SnapshotsTable(std::shared_ptr<const Table> table);
+};
+
+/// \brief Scan the snapshots metadata captured from a table.
+class ICEBERG_EXPORT SnapshotsScan final : public MetadataScan {
+ public:
+ /// \brief Create a scan from captured metadata table settings.
+ static Result<std::unique_ptr<SnapshotsScan>> Make(
+ internal::MetadataScanContext context);
+
+ ~SnapshotsScan() override;
+
+ Result<MetadataScanTaskStreamPtr> PlanTasksStream() const override;
+
+ private:
+ SnapshotsScan(internal::MetadataScanContext context,
+ std::shared_ptr<MetadataScanTask> task);
+
+ const std::shared_ptr<MetadataScanTask> task_;
};
} // namespace iceberg
diff --git a/src/iceberg/table.cc b/src/iceberg/table.cc
index e56746e3..0fa6bf40 100644
--- a/src/iceberg/table.cc
+++ b/src/iceberg/table.cc
@@ -25,6 +25,7 @@
#include "iceberg/data_table_scan.h"
#include "iceberg/incremental_append_scan.h"
#include "iceberg/incremental_changelog_scan.h"
+#include "iceberg/inspect/metadata_table.h"
#include "iceberg/labels.h"
#include "iceberg/location_provider.h"
#include "iceberg/partition_spec.h"
@@ -294,6 +295,10 @@ Result<std::shared_ptr<SnapshotManager>>
Table::NewSnapshotManager() {
return SnapshotManager::Make(shared_from_this());
}
+Result<std::unique_ptr<MetadataTable>> Table::Inspect(MetadataTableKind kind)
const {
+ return MetadataTable::Make(shared_from_this(), kind);
+}
+
Result<std::shared_ptr<StagedTable>> StagedTable::Make(
TableIdentifier identifier, std::shared_ptr<TableMetadata> metadata,
std::string metadata_location, std::shared_ptr<FileIO> io,
diff --git a/src/iceberg/table.h b/src/iceberg/table.h
index 97b1a43a..50e6aa8f 100644
--- a/src/iceberg/table.h
+++ b/src/iceberg/table.h
@@ -221,6 +221,9 @@ class ICEBERG_EXPORT Table : public
std::enable_shared_from_this<Table> {
/// \brief Create a new SnapshotManager to manage snapshots and snapshot
references.
virtual Result<std::shared_ptr<SnapshotManager>> NewSnapshotManager();
+ /// \brief Construct a metadata table inspector for this table.
+ Result<std::unique_ptr<MetadataTable>> Inspect(MetadataTableKind kind) const;
+
protected:
Table(TableIdentifier identifier, std::shared_ptr<TableMetadata> metadata,
std::string metadata_location, std::shared_ptr<FileIO> io,
diff --git a/src/iceberg/test/arrow_test_util.h
b/src/iceberg/test/arrow_test_util.h
new file mode 100644
index 00000000..3f3d58c3
--- /dev/null
+++ b/src/iceberg/test/arrow_test_util.h
@@ -0,0 +1,87 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <memory>
+#include <string_view>
+#include <utility>
+
+#include <arrow/array.h>
+#include <arrow/c/bridge.h>
+#include <arrow/json/from_string.h>
+#include <arrow/record_batch.h>
+#include <arrow/table.h>
+#include <gtest/gtest.h>
+
+#include "iceberg/arrow/arrow_status_internal.h"
+#include "iceberg/arrow_c_data.h"
+#include "iceberg/result.h"
+#include "iceberg/schema.h"
+#include "iceberg/schema_internal.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+/// \brief Consume an Arrow C stream into a table, retaining the stream's
schema.
+inline Result<std::shared_ptr<::arrow::Table>>
ReadArrowTable(ArrowArrayStream&& stream) {
+ auto reader = ::arrow::ImportRecordBatchReader(&stream);
+ if (!reader.ok()) {
+ if (stream.release != nullptr) {
+ stream.release(&stream);
+ }
+ return InvalidArrowData(reader.status().ToString());
+ }
+ ICEBERG_ARROW_ASSIGN_OR_RETURN(auto table, reader.ValueUnsafe()->ToTable());
+ return table;
+}
+
+/// \brief Build an expected Arrow table from an Iceberg schema and JSON rows.
+inline Result<std::shared_ptr<::arrow::Table>> TableFromJson(const Schema&
schema,
+ std::string_view
rows) {
+ ArrowSchema c_schema{};
+ ICEBERG_RETURN_UNEXPECTED(ToArrowSchema(schema, &c_schema));
+ ICEBERG_ARROW_ASSIGN_OR_RETURN(auto arrow_schema,
::arrow::ImportSchema(&c_schema));
+ ICEBERG_ARROW_ASSIGN_OR_RETURN(
+ auto array,
+
::arrow::json::ArrayFromJSONString(::arrow::struct_(arrow_schema->fields()),
rows));
+ const auto& struct_array = static_cast<const ::arrow::StructArray&>(*array);
+ auto batch =
+ ::arrow::RecordBatch::Make(arrow_schema, array->length(),
struct_array.fields());
+ ICEBERG_ARROW_ASSIGN_OR_RETURN(
+ auto table, ::arrow::Table::FromRecordBatches(arrow_schema,
{std::move(batch)}));
+ return table;
+}
+
+/// \brief Compare schemas and values independently of Arrow chunk boundaries.
+inline ::testing::AssertionResult TablesEqual(const ::arrow::Table& expected,
+ const ::arrow::Table& actual) {
+ const auto status = actual.ValidateFull();
+ if (!status.ok()) {
+ return ::testing::AssertionFailure() << status.ToString();
+ }
+ if (expected.Equals(actual, /*check_metadata=*/true)) {
+ return ::testing::AssertionSuccess();
+ }
+ return ::testing::AssertionFailure() << "Expected:\n"
+ << expected.ToString() << "\nActual:\n"
+ << actual.ToString();
+}
+
+} // namespace iceberg
diff --git a/src/iceberg/test/history_table_test.cc
b/src/iceberg/test/history_table_test.cc
index 8da311c0..1dbcdefb 100644
--- a/src/iceberg/test/history_table_test.cc
+++ b/src/iceberg/test/history_table_test.cc
@@ -17,39 +17,174 @@
* under the License.
*/
-/// \file history_table_test.cc
-/// Unit tests for HistoryTable.
-
#include "iceberg/inspect/history_table.h"
-#include <gmock/gmock.h>
+#include <cstdint>
+#include <memory>
+#include <utility>
+#include <vector>
+
#include <gtest/gtest.h>
-#include "iceberg/inspect/metadata_table.h"
+#include "iceberg/inspect/metadata_scan_task_reader.h"
#include "iceberg/schema.h"
#include "iceberg/schema_field.h"
+#include "iceberg/snapshot.h"
+#include "iceberg/table_metadata.h"
+#include "iceberg/test/arrow_test_util.h"
#include "iceberg/test/matchers.h"
#include "iceberg/test/metadata_table_test_base.h"
#include "iceberg/type.h"
+#include "iceberg/util/timepoint.h"
namespace iceberg {
-namespace {
-
-std::shared_ptr<Schema> MakeHistorySchema() {
- return std::make_shared<Schema>(std::vector<SchemaField>{
- SchemaField::MakeRequired(1, "made_current_at", timestamp_tz()),
- SchemaField::MakeRequired(2, "snapshot_id", int64()),
- SchemaField::MakeOptional(3, "parent_id", int64()),
- SchemaField::MakeRequired(4, "is_current_ancestor", boolean())});
+
+class HistoryTableTest : public MetadataTableTestBase {
+ protected:
+ void SetUp() override {
+ MetadataTableTestBase::SetUp();
+ metadata_->snapshots = {
+ std::make_shared<Snapshot>(Snapshot{.snapshot_id = 1}),
+ std::make_shared<Snapshot>(Snapshot{.snapshot_id = 2,
.parent_snapshot_id = 1}),
+ std::make_shared<Snapshot>(Snapshot{.snapshot_id = 3,
.parent_snapshot_id = 1})};
+ metadata_->current_snapshot_id = 3;
+ // Roll back from 2 to 1, include an expired snapshot (4), then move to 3.
+ metadata_->snapshot_log = {
+ {.timestamp_ms = TimePointMsFromUnixMs(1000), .snapshot_id = 1},
+ {.timestamp_ms = TimePointMsFromUnixMs(2000), .snapshot_id = 2},
+ {.timestamp_ms = TimePointMsFromUnixMs(2500), .snapshot_id = 1},
+ {.timestamp_ms = TimePointMsFromUnixMs(2700), .snapshot_id = 4},
+ {.timestamp_ms = TimePointMsFromUnixMs(3000), .snapshot_id = 3}};
+ ICEBERG_UNWRAP_OR_FAIL(metadata_table_,
table_->Inspect(MetadataTableKind::kHistory));
+ }
+
+ static std::shared_ptr<Schema> ExpectedSchema() {
+ return std::make_shared<Schema>(std::vector<SchemaField>{
+ SchemaField::MakeRequired(1, "made_current_at", timestamp_tz()),
+ SchemaField::MakeRequired(2, "snapshot_id", int64()),
+ SchemaField::MakeOptional(3, "parent_id", int64()),
+ SchemaField::MakeRequired(4, "is_current_ancestor", boolean())});
+ }
+
+ std::unique_ptr<MetadataTable> metadata_table_;
+};
+
+TEST_F(HistoryTableTest, CanonicalSchema) {
+ const auto expected = ExpectedSchema();
+ EXPECT_EQ(*HistoryTable::CanonicalSchema(), *expected);
+ EXPECT_EQ(*metadata_table_->schema(), *expected);
}
-} // namespace
+TEST_F(HistoryTableTest, Read) {
+ ICEBERG_UNWRAP_OR_FAIL(auto builder, metadata_table_->NewScan());
+ ICEBERG_UNWRAP_OR_FAIL(auto scan, builder->Build());
+ ICEBERG_UNWRAP_OR_FAIL(auto tasks, scan->PlanTasks());
+ ASSERT_EQ(tasks.size(), 1);
+ ICEBERG_UNWRAP_OR_FAIL(auto expected, TableFromJson(*ExpectedSchema(), R"([
+
{"made_current_at":1000000,"snapshot_id":1,"parent_id":null,"is_current_ancestor":true},
+
{"made_current_at":2000000,"snapshot_id":2,"parent_id":1,"is_current_ancestor":false},
+
{"made_current_at":2500000,"snapshot_id":1,"parent_id":null,"is_current_ancestor":true},
+
{"made_current_at":2700000,"snapshot_id":4,"parent_id":null,"is_current_ancestor":false},
+
{"made_current_at":3000000,"snapshot_id":3,"parent_id":1,"is_current_ancestor":true}
+ ])"));
+ for (int64_t batch_size : {1, 2}) {
+ SCOPED_TRACE(batch_size);
+ ICEBERG_UNWRAP_OR_FAIL(auto reader,
+ MetadataScanTaskReader::Make({.batch_size =
batch_size}));
+ ICEBERG_UNWRAP_OR_FAIL(auto stream, reader->Open(*tasks.front()));
+ ICEBERG_UNWRAP_OR_FAIL(auto actual, ReadArrowTable(std::move(stream)));
+ EXPECT_TRUE(TablesEqual(*expected, *actual));
+ }
+}
-class HistoryTableTest : public MetadataTableTestBase {};
+TEST_F(HistoryTableTest, Project) {
+ ICEBERG_UNWRAP_OR_FAIL(auto builder, metadata_table_->NewScan());
+ builder->Select({"snapshot_id", "is_current_ancestor"});
+ ICEBERG_UNWRAP_OR_FAIL(auto selected_scan, builder->Build());
+
+ auto projection = std::make_shared<Schema>(
+ std::vector<SchemaField>{SchemaField::MakeOptional(3, "parent_id",
int64()),
+ SchemaField::MakeRequired(2, "snapshot_id",
int64())});
+ builder->Project(projection);
+ ICEBERG_UNWRAP_OR_FAIL(auto projected_scan, builder->Build());
+
+ Schema selection({SchemaField::MakeRequired(2, "snapshot_id", int64()),
+ SchemaField::MakeRequired(4, "is_current_ancestor",
boolean())});
+ ICEBERG_UNWRAP_OR_FAIL(auto expected, TableFromJson(selection, R"([
+ {"snapshot_id":1,"is_current_ancestor":true},
+ {"snapshot_id":2,"is_current_ancestor":false},
+ {"snapshot_id":1,"is_current_ancestor":true},
+ {"snapshot_id":4,"is_current_ancestor":false},
+ {"snapshot_id":3,"is_current_ancestor":true}
+ ])"));
+ ICEBERG_UNWRAP_OR_FAIL(auto stream, selected_scan->ToArrowArrayStream());
+ ICEBERG_UNWRAP_OR_FAIL(auto actual, ReadArrowTable(std::move(stream)));
+ EXPECT_TRUE(TablesEqual(*expected, *actual));
+
+ ICEBERG_UNWRAP_OR_FAIL(auto reordered, TableFromJson(*projection, R"([
+ {"parent_id":null,"snapshot_id":1},
+ {"parent_id":1,"snapshot_id":2},
+ {"parent_id":null,"snapshot_id":1},
+ {"parent_id":null,"snapshot_id":4},
+ {"parent_id":1,"snapshot_id":3}
+ ])"));
+ ICEBERG_UNWRAP_OR_FAIL(auto tasks, projected_scan->PlanTasks());
+ ASSERT_EQ(tasks.size(), 1);
+ ICEBERG_UNWRAP_OR_FAIL(auto reader,
MetadataScanTaskReader::Make({.batch_size = 2}));
+ ICEBERG_UNWRAP_OR_FAIL(auto projected_stream, reader->Open(*tasks.front()));
+ ICEBERG_UNWRAP_OR_FAIL(auto projected,
ReadArrowTable(std::move(projected_stream)));
+ EXPECT_TRUE(TablesEqual(*reordered, *projected));
+}
+
+TEST_F(HistoryTableTest, MissingSnapshotsAreNotAncestors) {
+ // Snapshot 4 is in the history log but no longer exists in snapshots.
+ for (bool missing_current : {true, false}) {
+ SCOPED_TRACE(missing_current);
+ metadata_->current_snapshot_id = missing_current ? 4 : 3;
+ metadata_->snapshots.back()->parent_snapshot_id = 4;
+ ICEBERG_UNWRAP_OR_FAIL(auto builder, metadata_table_->NewScan());
+ builder->Select({"snapshot_id", "is_current_ancestor"});
+ ICEBERG_UNWRAP_OR_FAIL(auto scan, builder->Build());
+ ICEBERG_UNWRAP_OR_FAIL(auto stream, scan->ToArrowArrayStream());
+ ICEBERG_UNWRAP_OR_FAIL(auto actual, ReadArrowTable(std::move(stream)));
+ Schema schema({SchemaField::MakeRequired(2, "snapshot_id", int64()),
+ SchemaField::MakeRequired(4, "is_current_ancestor",
boolean())});
+ const auto rows = missing_current ? R"([
+ {"snapshot_id":1,"is_current_ancestor":false},
+ {"snapshot_id":2,"is_current_ancestor":false},
+ {"snapshot_id":1,"is_current_ancestor":false},
+ {"snapshot_id":4,"is_current_ancestor":false},
+ {"snapshot_id":3,"is_current_ancestor":false}
+ ])"
+ : R"([
+ {"snapshot_id":1,"is_current_ancestor":false},
+ {"snapshot_id":2,"is_current_ancestor":false},
+ {"snapshot_id":1,"is_current_ancestor":false},
+ {"snapshot_id":4,"is_current_ancestor":false},
+ {"snapshot_id":3,"is_current_ancestor":true}
+ ])";
+ ICEBERG_UNWRAP_OR_FAIL(auto expected, TableFromJson(schema, rows));
+ EXPECT_TRUE(TablesEqual(*expected, *actual));
+ }
+}
-TEST_F(HistoryTableTest, SchemaMatchesIcebergSchema) {
- ICEBERG_UNWRAP_OR_FAIL(auto history_table,
MetadataTable::Make<HistoryTable>(table_));
- EXPECT_TRUE(*history_table->schema() == *MakeHistorySchema());
+TEST_F(HistoryTableTest, CyclicAncestryTerminates) {
+ metadata_->snapshots.front()->parent_snapshot_id = 3;
+ ICEBERG_UNWRAP_OR_FAIL(auto builder, metadata_table_->NewScan());
+ builder->Select({"snapshot_id", "is_current_ancestor"});
+ ICEBERG_UNWRAP_OR_FAIL(auto scan, builder->Build());
+ ICEBERG_UNWRAP_OR_FAIL(auto stream, scan->ToArrowArrayStream());
+ ICEBERG_UNWRAP_OR_FAIL(auto actual, ReadArrowTable(std::move(stream)));
+ Schema schema({SchemaField::MakeRequired(2, "snapshot_id", int64()),
+ SchemaField::MakeRequired(4, "is_current_ancestor",
boolean())});
+ ICEBERG_UNWRAP_OR_FAIL(auto expected, TableFromJson(schema, R"([
+ {"snapshot_id":1,"is_current_ancestor":true},
+ {"snapshot_id":2,"is_current_ancestor":false},
+ {"snapshot_id":1,"is_current_ancestor":true},
+ {"snapshot_id":4,"is_current_ancestor":false},
+ {"snapshot_id":3,"is_current_ancestor":true}
+ ])"));
+ EXPECT_TRUE(TablesEqual(*expected, *actual));
}
} // namespace iceberg
diff --git a/src/iceberg/test/metadata_table_test.cc
b/src/iceberg/test/metadata_table_test.cc
index 187646c9..7babd917 100644
--- a/src/iceberg/test/metadata_table_test.cc
+++ b/src/iceberg/test/metadata_table_test.cc
@@ -23,7 +23,6 @@
#include <memory>
#include <string>
#include <utility>
-#include <variant>
#include <vector>
#include <arrow/c/bridge.h>
@@ -32,78 +31,40 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>
-#include "iceberg/constants.h"
#include "iceberg/inspect/history_table.h"
+#include "iceberg/inspect/metadata_scan.h"
+#include "iceberg/inspect/metadata_scan_task.h"
#include "iceberg/inspect/snapshots_table.h"
#include "iceberg/schema.h"
#include "iceberg/schema_field.h"
#include "iceberg/table.h"
-#include "iceberg/table_identifier.h"
#include "iceberg/table_metadata.h"
+#include "iceberg/test/arrow_test_util.h"
#include "iceberg/test/matchers.h"
-#include "iceberg/test/mock_catalog.h"
-#include "iceberg/test/mock_io.h"
+#include "iceberg/test/metadata_table_test_base.h"
#include "iceberg/type.h"
-#include "iceberg/util/timepoint.h"
namespace iceberg {
namespace {
-/// \brief Time-travel table that records the arguments of each scan.
-class RecordingTimeTravelTable : public TimeTravelMetadataTable {
+class PlannedMetadataScan : public MetadataScan {
public:
- explicit RecordingTimeTravelTable(std::shared_ptr<Table> table)
- : TimeTravelMetadataTable(std::move(table)) {}
+ PlannedMetadataScan(internal::MetadataScanContext context,
+ std::vector<std::shared_ptr<MetadataScanTask>> tasks)
+ : MetadataScan(std::move(context)), tasks_(std::move(tasks)) {}
- Kind kind() const noexcept override { return Kind::kSnapshots; }
-
- const std::shared_ptr<Schema>& schema() const override {
- static const auto schema = std::make_shared<Schema>(
- std::vector<SchemaField>{SchemaField::MakeRequired(1, "id", int64()),
- SchemaField::MakeOptional(2, "name",
string())});
- return schema;
- }
-
- const SnapshotSelection& scanned_snapshot() const { return
scanned_snapshot_; }
- const std::vector<std::string>& scanned_columns() const { return
scanned_columns_; }
-
- protected:
- Result<ArrowArrayStream> ScanSnapshot(const SnapshotSelection&
snapshot_selection,
- const Schema& projected_schema) const
override {
- scanned_snapshot_ = snapshot_selection;
- scanned_columns_.clear();
- for (const auto& field : projected_schema.fields()) {
- scanned_columns_.emplace_back(field.name());
- }
- return NotSupported("Recording table does not return rows");
+ Result<MetadataScanTaskStreamPtr> PlanTasksStream() const override {
+ return MetadataScanTaskStream::FromVector(tasks_);
}
private:
- mutable SnapshotSelection scanned_snapshot_;
- mutable std::vector<std::string> scanned_columns_;
+ const std::vector<std::shared_ptr<MetadataScanTask>> tasks_;
};
} // namespace
-class MetadataTableTest : public ::testing::Test {
+class MetadataTableTest : public MetadataTableTestBase {
protected:
- void SetUp() override {
- auto schema = std::make_shared<Schema>(
- std::vector<SchemaField>{SchemaField::MakeRequired(1, "id", int64()),
- SchemaField::MakeOptional(2, "name",
string())},
- 1);
- auto metadata = std::make_shared<TableMetadata>(
- TableMetadata{.format_version = 2,
- .schemas = {schema},
- .current_schema_id = 1,
- .current_snapshot_id = kInvalidSnapshotId});
-
- TableIdentifier ident{.ns = Namespace{.levels = {"db"}}, .name =
"source_table"};
- ICEBERG_UNWRAP_OR_FAIL(table_, Table::Make(ident, metadata,
"s3://bucket/meta.json",
- std::make_shared<MockFileIO>(),
-
std::make_shared<MockCatalog>()));
- }
-
/// \brief Import the schema of a stream and release the stream.
static Result<std::shared_ptr<::arrow::Schema>>
ReadSchema(ArrowArrayStream&& stream) {
auto reader_result = ::arrow::ImportRecordBatchReader(&stream);
@@ -115,8 +76,6 @@ class MetadataTableTest : public ::testing::Test {
}
return reader_result.ValueUnsafe()->schema();
}
-
- std::shared_ptr<Table> table_;
};
TEST_F(MetadataTableTest, FactoryRejectsNullSourceTable) {
@@ -131,15 +90,15 @@ TEST_F(MetadataTableTest,
ScanBuilderSelectResolvesNamesImmediately) {
// Select() uses the case sensitivity in effect when it is called.
ICEBERG_UNWRAP_OR_FAIL(auto builder, snapshots_table->NewScan());
- ICEBERG_UNWRAP_OR_FAIL(auto stream,
-
builder->Select({"SNAPSHOT_ID"}).CaseSensitive(false).Build());
+ ICEBERG_UNWRAP_OR_FAIL(
+ auto stream,
BuildStream(builder->Select({"SNAPSHOT_ID"}).CaseSensitive(false)));
ICEBERG_UNWRAP_OR_FAIL(auto schema, ReadSchema(std::move(stream)));
EXPECT_EQ(schema->num_fields(), 0);
ICEBERG_UNWRAP_OR_FAIL(auto insensitive_builder, snapshots_table->NewScan());
ICEBERG_UNWRAP_OR_FAIL(
auto insensitive_stream,
-
insensitive_builder->CaseSensitive(false).Select({"SNAPSHOT_ID"}).Build());
+
BuildStream(insensitive_builder->CaseSensitive(false).Select({"SNAPSHOT_ID"})));
ICEBERG_UNWRAP_OR_FAIL(auto insensitive_schema,
ReadSchema(std::move(insensitive_stream)));
EXPECT_THAT(insensitive_schema->field_names(),
::testing::ElementsAre("snapshot_id"));
@@ -160,8 +119,8 @@ TEST_F(MetadataTableTest, ScanBuilderProject) {
// Project() replaces an earlier selection.
ICEBERG_UNWRAP_OR_FAIL(auto builder, snapshots_table->NewScan());
- ICEBERG_UNWRAP_OR_FAIL(auto stream,
-
builder->Select({"snapshot_id"}).Project(projection).Build());
+ ICEBERG_UNWRAP_OR_FAIL(
+ auto stream,
BuildStream(builder->Select({"snapshot_id"}).Project(projection)));
ICEBERG_UNWRAP_OR_FAIL(auto schema, ReadSchema(std::move(stream)));
EXPECT_THAT(schema->field_names(), ::testing::ElementsAre("operation",
"snapshot_id"));
@@ -170,73 +129,30 @@ TEST_F(MetadataTableTest, ScanBuilderProject) {
IsError(ErrorKind::kInvalidArgument));
}
-TEST_F(MetadataTableTest, SupportsTimeTravel) {
- ICEBERG_UNWRAP_OR_FAIL(auto snapshots_table,
- MetadataTable::Make<SnapshotsTable>(table_));
- EXPECT_FALSE(snapshots_table->supports_time_travel());
-
- ICEBERG_UNWRAP_OR_FAIL(auto history_table,
MetadataTable::Make<HistoryTable>(table_));
- EXPECT_FALSE(history_table->supports_time_travel());
-
- RecordingTimeTravelTable time_travel_table(table_);
- EXPECT_TRUE(time_travel_table.supports_time_travel());
-}
-
-TEST_F(MetadataTableTest, TimeTravelScanDefaults) {
- RecordingTimeTravelTable table(table_);
- ICEBERG_UNWRAP_OR_FAIL(auto builder, table.NewScan());
- EXPECT_THAT(builder->Build(), IsError(ErrorKind::kNotSupported));
-
EXPECT_TRUE(std::holds_alternative<std::monostate>(table.scanned_snapshot().snapshot));
- EXPECT_TRUE(table.scanned_snapshot().ref_name.empty());
- EXPECT_THAT(table.scanned_columns(), ::testing::ElementsAre("id", "name"));
-}
-
-TEST_F(MetadataTableTest, TimeTravelScanSelectsColumnsAndSnapshot) {
- RecordingTimeTravelTable table(table_);
- ICEBERG_UNWRAP_OR_FAIL(auto builder, table.NewScan());
- EXPECT_THAT(builder->Select({"name"}).UseSnapshot(1).UseRef("dev").Build(),
- IsError(ErrorKind::kNotSupported));
-
ASSERT_TRUE(std::holds_alternative<int64_t>(table.scanned_snapshot().snapshot));
- EXPECT_EQ(std::get<int64_t>(table.scanned_snapshot().snapshot), 1);
- EXPECT_EQ(table.scanned_snapshot().ref_name, "dev");
- EXPECT_THAT(table.scanned_columns(), ::testing::ElementsAre("name"));
-}
-
-TEST_F(MetadataTableTest, TimeTravelScanAsOfTime) {
- RecordingTimeTravelTable table(table_);
- ICEBERG_UNWRAP_OR_FAIL(auto builder, table.NewScan());
- EXPECT_THAT(builder->AsOfTime(1234567890000).Build(),
- IsError(ErrorKind::kNotSupported));
-
ASSERT_TRUE(std::holds_alternative<TimePointMs>(table.scanned_snapshot().snapshot));
- EXPECT_EQ(std::get<TimePointMs>(table.scanned_snapshot().snapshot),
- TimePointMsFromUnixMs(1234567890000));
- EXPECT_TRUE(table.scanned_snapshot().ref_name.empty());
- EXPECT_THAT(table.scanned_columns(), ::testing::ElementsAre("id", "name"));
-}
-
TEST_F(MetadataTableTest, ScanBuilderRejectsConflictingSnapshotOptions) {
- RecordingTimeTravelTable table(table_);
+ ICEBERG_UNWRAP_OR_FAIL(auto table,
MetadataTable::Make<SnapshotsTable>(table_));
- ICEBERG_UNWRAP_OR_FAIL(auto snapshot_builder, table.NewScan());
+ ICEBERG_UNWRAP_OR_FAIL(auto snapshot_builder, table->NewScan());
auto snapshot_result = snapshot_builder->UseSnapshot(1).AsOfTime(2).Build();
EXPECT_THAT(snapshot_result, IsError(ErrorKind::kValidationFailed));
EXPECT_THAT(snapshot_result, HasErrorMessage("Cannot override snapshot"));
- ICEBERG_UNWRAP_OR_FAIL(auto ref_builder, table.NewScan());
+ ICEBERG_UNWRAP_OR_FAIL(auto ref_builder, table->NewScan());
auto ref_result = ref_builder->UseRef("dev").UseRef("main").Build();
EXPECT_THAT(ref_result, IsError(ErrorKind::kValidationFailed));
EXPECT_THAT(ref_result, HasErrorMessage("Cannot override ref"));
- ICEBERG_UNWRAP_OR_FAIL(auto empty_ref_builder, table.NewScan());
+ ICEBERG_UNWRAP_OR_FAIL(auto empty_ref_builder, table->NewScan());
EXPECT_THAT(empty_ref_builder->UseRef("").Build(),
IsError(ErrorKind::kValidationFailed));
}
-TEST_F(MetadataTableTest, HistoryTableScanIsNotSupported) {
+TEST_F(MetadataTableTest, HistoryTableScanAndTimeTravelSelection) {
ICEBERG_UNWRAP_OR_FAIL(auto history_table,
MetadataTable::Make<HistoryTable>(table_));
ICEBERG_UNWRAP_OR_FAIL(auto builder, history_table->NewScan());
- EXPECT_THAT(builder->Select({"snapshot_id"}).Build(),
- IsError(ErrorKind::kNotSupported));
+ ICEBERG_UNWRAP_OR_FAIL(auto stream,
BuildStream(builder->Select({"snapshot_id"})));
+ ICEBERG_UNWRAP_OR_FAIL(auto schema, ReadSchema(std::move(stream)));
+ EXPECT_THAT(schema->field_names(), ::testing::ElementsAre("snapshot_id"));
// Tables without time travel reject snapshot selections.
ICEBERG_UNWRAP_OR_FAIL(auto snapshot_builder, history_table->NewScan());
@@ -245,4 +161,33 @@ TEST_F(MetadataTableTest, HistoryTableScanIsNotSupported) {
EXPECT_THAT(snapshot_result, HasErrorMessage("does not support time
travel"));
}
+TEST_F(MetadataTableTest, ScanReadsEveryTaskInPlanningOrder) {
+ auto projection = std::make_shared<Schema>(
+ std::vector<SchemaField>{SchemaField::MakeRequired(2, "snapshot_id",
int64())});
+ auto metadata = std::make_shared<TableMetadata>();
+ metadata->snapshots = {std::make_shared<Snapshot>(Snapshot{.snapshot_id =
2})};
+ ICEBERG_UNWRAP_OR_FAIL(
+ auto first,
+ StaticMetadataTask::Make(MetadataTableKind::kSnapshots, metadata,
projection));
+ auto next_metadata = std::make_shared<TableMetadata>();
+ next_metadata->snapshots = {std::make_shared<Snapshot>(Snapshot{.snapshot_id
= 1})};
+ ICEBERG_UNWRAP_OR_FAIL(
+ auto second,
+ StaticMetadataTask::Make(MetadataTableKind::kSnapshots, next_metadata,
projection));
+ ICEBERG_UNWRAP_OR_FAIL(auto empty, StaticMetadataTask::Make(
+ MetadataTableKind::kSnapshots,
+ std::make_shared<TableMetadata>(),
projection));
+ PlannedMetadataScan scan({.kind = MetadataTableKind::kSnapshots,
+ .table_metadata = metadata,
+ .schema = SnapshotsTable::CanonicalSchema(),
+ .projected_schema = projection},
+ {empty, first, empty, second});
+ ICEBERG_UNWRAP_OR_FAIL(auto stream, scan.ToArrowArrayStream());
+ ICEBERG_UNWRAP_OR_FAIL(auto actual, ReadArrowTable(std::move(stream)));
+ ICEBERG_UNWRAP_OR_FAIL(
+ auto expected,
+ TableFromJson(*projection, R"([{"snapshot_id":2},{"snapshot_id":1}])"));
+ EXPECT_TRUE(TablesEqual(*expected, *actual));
+}
+
} // namespace iceberg
diff --git a/src/iceberg/test/metadata_table_test_base.h
b/src/iceberg/test/metadata_table_test_base.h
index cefdb5cb..de44c1d1 100644
--- a/src/iceberg/test/metadata_table_test_base.h
+++ b/src/iceberg/test/metadata_table_test_base.h
@@ -39,6 +39,7 @@
#include <gtest/gtest.h>
#include "iceberg/constants.h"
+#include "iceberg/inspect/metadata_scan.h"
#include "iceberg/inspect/metadata_table.h"
#include "iceberg/schema.h"
#include "iceberg/schema_field.h"
@@ -81,10 +82,16 @@ class MetadataTableTestBase : public ::testing::Test {
"s3://bucket/meta.json", io_,
catalog_));
}
+ /// \brief Build a scan and open its projected rows.
+ static Result<ArrowArrayStream> BuildStream(MetadataScanBuilder& builder) {
+ ICEBERG_ASSIGN_OR_RAISE(auto scan, builder.Build());
+ return scan->ToArrowArrayStream();
+ }
+
/// \brief Scan all columns of a metadata table.
static Result<ArrowArrayStream> Scan(MetadataTable& table) {
ICEBERG_ASSIGN_OR_RAISE(auto builder, table.NewScan());
- return builder->Build();
+ return BuildStream(*builder);
}
/// \brief Scan the selected columns of a metadata table.
@@ -92,7 +99,7 @@ class MetadataTableTestBase : public ::testing::Test {
const std::vector<std::string>&
columns,
bool case_sensitive = true) {
ICEBERG_ASSIGN_OR_RAISE(auto builder, table.NewScan());
- return builder->CaseSensitive(case_sensitive).Select(columns).Build();
+ return BuildStream(builder->CaseSensitive(case_sensitive).Select(columns));
}
/// \brief Import and consume a scan-produced ArrowArrayStream.
diff --git a/src/iceberg/test/snapshots_table_test.cc
b/src/iceberg/test/snapshots_table_test.cc
index 9a62c6b6..dea10a01 100644
--- a/src/iceberg/test/snapshots_table_test.cc
+++ b/src/iceberg/test/snapshots_table_test.cc
@@ -32,7 +32,9 @@
#include <gtest/gtest.h>
#include "iceberg/constants.h"
+#include "iceberg/inspect/metadata_scan_task_reader.h"
#include "iceberg/inspect/metadata_table.h"
+#include "iceberg/test/arrow_test_util.h"
#include "iceberg/test/matchers.h"
#include "iceberg/test/metadata_table_test_base.h"
@@ -194,7 +196,7 @@ TEST_F(SnapshotsTableTest, ScanBuildsRepeatedly) {
ICEBERG_UNWRAP_OR_FAIL(auto builder, snapshots_table_->NewScan());
builder->Select({"snapshot_id"});
for (int read = 0; read < 2; ++read) {
- ICEBERG_UNWRAP_OR_FAIL(auto stream, builder->Build());
+ ICEBERG_UNWRAP_OR_FAIL(auto stream, BuildStream(*builder));
ICEBERG_UNWRAP_OR_FAIL(auto batches, ReadAllBatches(std::move(stream)));
ASSERT_EQ(batches.size(), 1);
EXPECT_EQ(batches.front()->num_rows(), 2);
@@ -223,7 +225,7 @@ TEST_F(SnapshotsTableTest, ScanProjectedColumns) {
SchemaField::MakeRequired(8, "value",
string()))),
SchemaField::MakeRequired(2, "snapshot_id", int64())});
ICEBERG_UNWRAP_OR_FAIL(auto builder, snapshots_table_->NewScan());
- ICEBERG_UNWRAP_OR_FAIL(auto stream, builder->Project(projection).Build());
+ ICEBERG_UNWRAP_OR_FAIL(auto stream,
BuildStream(builder->Project(projection)));
ICEBERG_UNWRAP_OR_FAIL(auto batches, ReadAllBatches(std::move(stream)));
ASSERT_EQ(batches.size(), 1);
const auto& batch = batches.front();
@@ -320,6 +322,7 @@ TEST_F(SnapshotsTableTest, ScanSkipsNullSnapshots) {
TEST_F(SnapshotsTableTest, ScanEmptySummary) {
auto [missing_summary, operation_only_summary] = MakeTestSnapshots();
missing_summary->summary.clear();
+ missing_summary->manifest_list.clear();
operation_only_summary->summary = {
{SnapshotSummaryFields::kOperation, DataOperation::kAppend}};
ICEBERG_UNWRAP_OR_FAIL(auto table,
@@ -334,14 +337,18 @@ TEST_F(SnapshotsTableTest, ScanEmptySummary) {
auto summaries =
std::static_pointer_cast<::arrow::MapArray>(batches.front()->column(5));
EXPECT_TRUE(summaries->IsNull(0));
+ auto manifests =
+
std::static_pointer_cast<::arrow::StringArray>(batches.front()->column(4));
+ EXPECT_TRUE(manifests->IsNull(0));
+ EXPECT_FALSE(manifests->IsNull(1));
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);
+ std::vector<std::shared_ptr<Snapshot>> snapshots(
+ MetadataScanTaskReader::Options{}.batch_size + 1, snapshot);
ICEBERG_UNWRAP_OR_FAIL(auto table,
MakeTableWithSnapshots(std::move(snapshots),
/*current_snapshot_id=*/1));
ICEBERG_UNWRAP_OR_FAIL(auto snapshots_table,
@@ -350,14 +357,14 @@ TEST_F(SnapshotsTableTest, ScanReturnsMultipleBatches) {
ICEBERG_UNWRAP_OR_FAIL(auto stream, Scan(*snapshots_table));
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[0]->num_rows(),
MetadataScanTaskReader::Options{}.batch_size);
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);
+ std::vector<std::shared_ptr<Snapshot>> snapshots(
+ MetadataScanTaskReader::Options{}.batch_size + 1, snapshot);
ICEBERG_UNWRAP_OR_FAIL(auto table,
MakeTableWithSnapshots(std::move(snapshots),
/*current_snapshot_id=*/1));
ICEBERG_UNWRAP_OR_FAIL(auto snapshots_table,
@@ -371,7 +378,8 @@ TEST_F(SnapshotsTableTest,
ScanUsesMetadataCapturedAtCreation) {
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);
+ EXPECT_EQ((*first_batch_result)->num_rows(),
+ MetadataScanTaskReader::Options{}.batch_size);
ICEBERG_UNWRAP_OR_FAIL(
auto refreshed_table,
@@ -387,4 +395,50 @@ TEST_F(SnapshotsTableTest,
ScanUsesMetadataCapturedAtCreation) {
EXPECT_EQ((*second_batch_result)->num_rows(), 1);
}
+TEST_F(SnapshotsTableTest, CanonicalSchema) {
+ auto expected = std::make_shared<Schema>(std::vector<SchemaField>{
+ SchemaField::MakeRequired(1, "committed_at", timestamp_tz()),
+ SchemaField::MakeRequired(2, "snapshot_id", int64()),
+ SchemaField::MakeOptional(3, "parent_id", int64()),
+ SchemaField::MakeOptional(4, "operation", string()),
+ SchemaField::MakeOptional(5, "manifest_list", string()),
+ SchemaField::MakeOptional(
+ 6, "summary",
+ std::make_shared<MapType>(SchemaField::MakeRequired(7, "key",
string()),
+ SchemaField::MakeRequired(8, "value",
string())))});
+ EXPECT_EQ(*SnapshotsTable::CanonicalSchema(), *expected);
+ EXPECT_EQ(*snapshots_table_->schema(), *expected);
+}
+
+TEST_F(SnapshotsTableTest,
PlannedTaskRetainsProjectionAndIndependentReadState) {
+ ICEBERG_UNWRAP_OR_FAIL(auto builder, snapshots_table_->NewScan());
+ builder->Select({"snapshot_id", "operation"});
+ ICEBERG_UNWRAP_OR_FAIL(auto scan, builder->Build());
+ ICEBERG_UNWRAP_OR_FAIL(auto tasks, scan->PlanTasks());
+ ASSERT_EQ(tasks.size(), 1);
+ ICEBERG_UNWRAP_OR_FAIL(auto reader,
MetadataScanTaskReader::Make({.batch_size = 1}));
+ ICEBERG_UNWRAP_OR_FAIL(auto first, reader->Open(*tasks.front()));
+ ICEBERG_UNWRAP_OR_FAIL(auto second, reader->Open(*tasks.front()));
+ // Streams retain their own input and cursors after the planning objects are
gone.
+ tasks.clear();
+ scan.reset();
+ reader.reset();
+ builder.reset();
+ snapshots_table_.reset();
+ table_.reset();
+ Schema selected({SchemaField::MakeRequired(2, "snapshot_id", int64()),
+ SchemaField::MakeOptional(4, "operation", string())});
+ ICEBERG_UNWRAP_OR_FAIL(auto expected, TableFromJson(selected, R"([
+ {"snapshot_id":1,"operation":"append"},
+ {"snapshot_id":2,"operation":"append"}
+ ])"));
+ ICEBERG_UNWRAP_OR_FAIL(auto actual, ReadArrowTable(std::move(first)));
+ EXPECT_TRUE(TablesEqual(*expected, *actual));
+ ASSERT_EQ(actual->column(0)->num_chunks(), 2);
+ EXPECT_EQ(actual->column(0)->chunk(0)->length(), 1);
+ EXPECT_EQ(actual->column(0)->chunk(1)->length(), 1);
+ ICEBERG_UNWRAP_OR_FAIL(auto repeated, ReadArrowTable(std::move(second)));
+ EXPECT_TRUE(TablesEqual(*expected, *repeated));
+}
+
} // namespace iceberg
diff --git a/src/iceberg/test/stream_test.cc b/src/iceberg/test/stream_test.cc
index 91b35216..dbb9248a 100644
--- a/src/iceberg/test/stream_test.cc
+++ b/src/iceberg/test/stream_test.cc
@@ -104,6 +104,43 @@ static_assert(std::is_move_assignable_v<CopyOnlyStream>);
static_assert(std::is_move_constructible_v<MoveOnlyStream>);
static_assert(std::is_move_assignable_v<MoveOnlyStream>);
+TEST(StreamTest, FromVectorPreservesOrderAndEndOfStream) {
+ auto stream = Stream<int>::FromVector({3, 1, 2});
+ ICEBERG_UNWRAP_OR_FAIL(auto first, stream->Next());
+ ASSERT_TRUE(first.has_value());
+ EXPECT_EQ(*first, 3);
+ ICEBERG_UNWRAP_OR_FAIL(auto remaining, stream->ToVector());
+ EXPECT_EQ(remaining, (std::vector<int>{1, 2}));
+ for (int i = 0; i < 2; ++i) {
+ ICEBERG_UNWRAP_OR_FAIL(auto end, stream->Next());
+ EXPECT_FALSE(end.has_value());
+ }
+ auto empty = Stream<int>::FromVector({});
+ ICEBERG_UNWRAP_OR_FAIL(auto end, empty->Next());
+ EXPECT_FALSE(end.has_value());
+}
+
+TEST(StreamTest, FromVectorSupportsBooleans) {
+ auto stream = Stream<bool>::FromVector({true, false, true});
+ ICEBERG_UNWRAP_OR_FAIL(auto values, stream->ToVector());
+ EXPECT_EQ(values, (std::vector<bool>{true, false, true}));
+}
+
+TEST(StreamTest, FromVectorSupportsMoveAndCopyOnlyValues) {
+ std::vector<std::unique_ptr<int>> values;
+ values.push_back(std::make_unique<int>(42));
+ auto stream = Stream<std::unique_ptr<int>>::FromVector(std::move(values));
+ ICEBERG_UNWRAP_OR_FAIL(auto result, stream->ToVector());
+ ASSERT_EQ(result.size(), 1);
+ EXPECT_EQ(*result.front(), 42);
+
+ std::vector<CopyOnly> copy_values(1, CopyOnly(42));
+ auto copy_stream = Stream<CopyOnly>::FromVector(std::move(copy_values));
+ ICEBERG_UNWRAP_OR_FAIL(auto copied, copy_stream->ToVector());
+ ASSERT_EQ(copied.size(), 1);
+ EXPECT_EQ(copied.front().value(), 42);
+}
+
TEST(StreamTest, SupportsIncompleteSharedPointerValue) {
class IncompleteType;
std::unique_ptr<Stream<std::shared_ptr<IncompleteType>>> stream;
diff --git a/src/iceberg/type_fwd.h b/src/iceberg/type_fwd.h
index 9386c9dd..c0230feb 100644
--- a/src/iceberg/type_fwd.h
+++ b/src/iceberg/type_fwd.h
@@ -19,6 +19,8 @@
#pragma once
+#include <cstdint>
+
/// \file iceberg/type_fwd.h
/// Forward declarations and enum definitions. When writing your own headers,
/// you can include this instead of the "full" headers to help reduce compile
@@ -280,9 +282,14 @@ class DeleteLoader;
class PositionDeleteIndex;
/// \brief Metadata tables.
+enum class MetadataTableKind : uint8_t;
+
class HistoryTable;
class MetadataScanBuilder;
class MetadataTable;
+class MetadataScan;
+class MetadataScanTask;
+class MetadataScanTaskReader;
class SnapshotsTable;
/// \brief Table encryption
diff --git a/src/iceberg/util/stream.h b/src/iceberg/util/stream.h
index fc01e98c..1dc3055a 100644
--- a/src/iceberg/util/stream.h
+++ b/src/iceberg/util/stream.h
@@ -22,7 +22,9 @@
/// \file iceberg/util/stream.h
/// \brief Pull-based stream interface for fallible, lazily produced values.
+#include <cstddef>
#include <deque>
+#include <memory>
#include <optional>
#include <type_traits>
#include <utility>
@@ -62,6 +64,9 @@ class Stream {
/// \brief Move-assign a stream and its terminal state.
Stream& operator=(Stream&&) noexcept = default;
+ /// \brief Create a stream that owns and consumes the given values in order.
+ static std::unique_ptr<Stream<T>> FromVector(std::vector<T> values);
+
/// \brief Return the next value, or std::nullopt when the stream is
exhausted.
///
/// After this method returns an error or std::nullopt, subsequent calls
return the same
@@ -136,4 +141,27 @@ class Stream {
std::optional<Error> error_;
};
+template <typename T>
+std::unique_ptr<Stream<T>> Stream<T>::FromVector(std::vector<T> values) {
+ class VectorStream final : public Stream<T> {
+ public:
+ explicit VectorStream(std::vector<T> values) : values_(std::move(values))
{}
+
+ protected:
+ Result<std::optional<T>> NextImpl() override {
+ if (index_ == values_.size()) {
+ return std::nullopt;
+ }
+ auto&& value = values_[index_++];
+ return Result<std::optional<T>>(std::in_place, std::in_place,
+ std::move_if_noexcept(value));
+ }
+
+ private:
+ std::vector<T> values_;
+ size_t index_ = 0;
+ };
+ return std::make_unique<VectorStream>(std::move(values));
+}
+
} // namespace iceberg