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 d0ca49fc feat(inspect): support selecting columns in metadata table
scans (#1008)
d0ca49fc is described below
commit d0ca49fc0c089d0ed4c16b4c4eadcc22faba983f
Author: Manu Zhang <[email protected]>
AuthorDate: Fri Oct 30 22:29:43 2026 +0800
feat(inspect): support selecting columns in metadata table scans (#1008)
---
src/iceberg/CMakeLists.txt | 1 +
src/iceberg/cpp20_compatibility_internal.h | 1 +
src/iceberg/inspect/history_table.cc | 3 +-
src/iceberg/inspect/history_table.h | 3 +-
src/iceberg/inspect/metadata_scan.cc | 88 +++++++++++
src/iceberg/inspect/metadata_scan.h | 136 ++++++++++++++++
src/iceberg/inspect/metadata_table.cc | 26 +++-
src/iceberg/inspect/metadata_table.h | 71 +++++----
.../inspect/metadata_table_columns_internal.h | 103 +++++++++++++
src/iceberg/inspect/snapshots_table.cc | 93 +++++++----
src/iceberg/inspect/snapshots_table.h | 3 +-
src/iceberg/test/metadata_table_test.cc | 171 +++++++++++++++++++++
src/iceberg/test/metadata_table_test_base.h | 17 +-
src/iceberg/test/snapshots_table_test.cc | 170 +++++++++++++++++++-
src/iceberg/type_fwd.h | 1 +
15 files changed, 804 insertions(+), 83 deletions(-)
diff --git a/src/iceberg/CMakeLists.txt b/src/iceberg/CMakeLists.txt
index 06e59988..cf465e5f 100644
--- a/src/iceberg/CMakeLists.txt
+++ b/src/iceberg/CMakeLists.txt
@@ -59,6 +59,7 @@ set(ICEBERG_SOURCES
incremental_append_scan.cc
incremental_changelog_scan.cc
inspect/history_table.cc
+ inspect/metadata_scan.cc
inspect/metadata_table.cc
inspect/snapshots_table.cc
inheritable_metadata.cc
diff --git a/src/iceberg/cpp20_compatibility_internal.h
b/src/iceberg/cpp20_compatibility_internal.h
index 12114d8f..d186e5bb 100644
--- a/src/iceberg/cpp20_compatibility_internal.h
+++ b/src/iceberg/cpp20_compatibility_internal.h
@@ -81,6 +81,7 @@
#include "iceberg/incremental_changelog_scan.h"
#include "iceberg/inheritable_metadata.h"
#include "iceberg/inspect/history_table.h"
+#include "iceberg/inspect/metadata_scan.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 03bbcca9..518073ca 100644
--- a/src/iceberg/inspect/history_table.cc
+++ b/src/iceberg/inspect/history_table.cc
@@ -50,7 +50,8 @@ Result<std::unique_ptr<HistoryTable>>
HistoryTable::Make(std::shared_ptr<Table>
return std::unique_ptr<HistoryTable>(new HistoryTable(std::move(table)));
}
-Result<ArrowArrayStream> HistoryTable::Scan() {
+Result<ArrowArrayStream> HistoryTable::ScanProjection(
+ const Schema& /*projected_schema*/) const {
return NotSupported("Scan is not supported for the history table");
}
diff --git a/src/iceberg/inspect/history_table.h
b/src/iceberg/inspect/history_table.h
index 7d863fc2..6f07bcd0 100644
--- a/src/iceberg/inspect/history_table.h
+++ b/src/iceberg/inspect/history_table.h
@@ -42,7 +42,8 @@ class ICEBERG_EXPORT HistoryTable : public MetadataTable {
const std::shared_ptr<Schema>& schema() const override;
- Result<ArrowArrayStream> Scan() override;
+ protected:
+ Result<ArrowArrayStream> ScanProjection(const Schema& projected_schema)
const override;
private:
explicit HistoryTable(std::shared_ptr<Table> table);
diff --git a/src/iceberg/inspect/metadata_scan.cc
b/src/iceberg/inspect/metadata_scan.cc
new file mode 100644
index 00000000..6117c28c
--- /dev/null
+++ b/src/iceberg/inspect/metadata_scan.cc
@@ -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.
+ */
+
+#include "iceberg/inspect/metadata_scan.h"
+
+#include <memory>
+#include <utility>
+
+#include "iceberg/inspect/metadata_table.h"
+#include "iceberg/schema.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+MetadataScanBuilder::MetadataScanBuilder(const MetadataTable& table)
+ : table_(&table), projected_schema_(table.schema()) {}
+
+MetadataScanBuilder::~MetadataScanBuilder() = default;
+
+MetadataScanBuilder& MetadataScanBuilder::Select(
+ const std::vector<std::string>& columns) {
+ auto projected = table_->schema()->Select(columns, case_sensitive_);
+ if (projected) {
+ projected_schema_ = std::move(projected).value();
+ } else {
+ AddError(projected.error());
+ }
+ return *this;
+}
+
+MetadataScanBuilder& MetadataScanBuilder::Project(std::shared_ptr<Schema>
schema) {
+ projected_schema_ = std::move(schema);
+ return *this;
+}
+
+MetadataScanBuilder& MetadataScanBuilder::CaseSensitive(bool value) {
+ case_sensitive_ = value;
+ return *this;
+}
+
+MetadataScanBuilder& MetadataScanBuilder::UseSnapshot(int64_t snapshot_id) {
+ ICEBERG_BUILDER_CHECK(
+ std::holds_alternative<std::monostate>(snapshot_selection_.snapshot),
+ "Cannot override snapshot, a snapshot or timestamp is already set");
+ snapshot_selection_.snapshot = 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(!ref.empty(), "Ref name cannot be empty");
+ snapshot_selection_.ref_name = ref;
+ return *this;
+}
+
+MetadataScanBuilder& MetadataScanBuilder::AsOfTime(int64_t timestamp_millis) {
+ ICEBERG_BUILDER_CHECK(
+ std::holds_alternative<std::monostate>(snapshot_selection_.snapshot),
+ "Cannot override snapshot, a snapshot or timestamp is already set");
+ snapshot_selection_.snapshot = TimePointMsFromUnixMs(timestamp_millis);
+ return *this;
+}
+
+Result<ArrowArrayStream> MetadataScanBuilder::Build() {
+ ICEBERG_RETURN_UNEXPECTED(CheckErrors());
+ ICEBERG_PRECHECK(projected_schema_ != nullptr, "Projected schema cannot be
null");
+ return table_->ScanSnapshot(snapshot_selection_, *projected_schema_);
+}
+
+} // namespace iceberg
diff --git a/src/iceberg/inspect/metadata_scan.h
b/src/iceberg/inspect/metadata_scan.h
new file mode 100644
index 00000000..f487bce4
--- /dev/null
+++ b/src/iceberg/inspect/metadata_scan.h
@@ -0,0 +1,136 @@
+/*
+ * 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
+
+/// \file iceberg/inspect/metadata_scan.h
+/// \brief Scan configuration for Iceberg metadata tables.
+
+#include <cstdint>
+#include <memory>
+#include <string>
+#include <variant>
+#include <vector>
+
+#include "iceberg/arrow_c_data.h"
+#include "iceberg/iceberg_export.h"
+#include "iceberg/result.h"
+#include "iceberg/type_fwd.h"
+#include "iceberg/util/error_collector.h"
+#include "iceberg/util/timepoint.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;
+
+ /// \brief Resolve the snapshot relative to this branch or tag.
+ ///
+ /// An empty string uses the main branch.
+ std::string ref_name;
+};
+
+/// \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
+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.
+ ///
+ /// \param columns Column names from the metadata table's schema.
+ 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.
+ ///
+ /// \param schema The output schema.
+ 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.
+ 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.
+ 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.
+ 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.
+ ///
+ /// \param timestamp_millis A timestamp in milliseconds since the Unix epoch.
+ MetadataScanBuilder& AsOfTime(int64_t timestamp_millis);
+
+ /// \brief Validate the settings and scan the metadata table.
+ ///
+ /// 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();
+
+ 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_;
+};
+
+} // namespace iceberg
diff --git a/src/iceberg/inspect/metadata_table.cc
b/src/iceberg/inspect/metadata_table.cc
index 7bc94c51..3e21688f 100644
--- a/src/iceberg/inspect/metadata_table.cc
+++ b/src/iceberg/inspect/metadata_table.cc
@@ -21,6 +21,9 @@
#include <memory>
#include <utility>
+#include <variant>
+
+#include "iceberg/inspect/metadata_scan.h"
namespace iceberg {
@@ -35,6 +38,19 @@ const std::shared_ptr<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)) {}
@@ -42,13 +58,9 @@ TimeTravelMetadataTable::~TimeTravelMetadataTable() =
default;
bool TimeTravelMetadataTable::supports_time_travel() const noexcept { return
true; }
-Result<ArrowArrayStream> TimeTravelMetadataTable::Scan() {
- return ScanSnapshot(SnapshotSelection{});
-}
-
-Result<ArrowArrayStream> TimeTravelMetadataTable::Scan(
- const SnapshotSelection& snapshot_selection) {
- return ScanSnapshot(snapshot_selection);
+Result<ArrowArrayStream> TimeTravelMetadataTable::ScanProjection(
+ const Schema& projected_schema) const {
+ return ScanSnapshot(SnapshotSelection{}, projected_schema);
}
} // namespace iceberg
diff --git a/src/iceberg/inspect/metadata_table.h
b/src/iceberg/inspect/metadata_table.h
index 85000ee1..7717ce58 100644
--- a/src/iceberg/inspect/metadata_table.h
+++ b/src/iceberg/inspect/metadata_table.h
@@ -24,15 +24,13 @@
#include <concepts>
#include <memory>
-#include <string>
#include <utility>
-#include <variant>
#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"
-#include "iceberg/util/timepoint.h"
namespace iceberg {
@@ -64,7 +62,7 @@ class ICEBERG_EXPORT MetadataTable {
/// \brief Return this metadata table's kind.
virtual Kind kind() const noexcept = 0;
- /// \brief Return the schema of rows emitted by scans.
+ /// \brief Return the complete canonical schema of this metadata table.
virtual const std::shared_ptr<Schema>& schema() const = 0;
/// \brief Return the source table whose metadata is exposed.
@@ -73,36 +71,44 @@ class ICEBERG_EXPORT MetadataTable {
/// \brief Return whether this metadata table supports time travel.
virtual bool supports_time_travel() const noexcept;
- /// \brief Scan the metadata table without time travel.
+ /// \brief Create a builder for configuring a metadata table scan.
///
- /// The caller owns the returned stream and must invoke its `release`
callback
- /// when the stream is no longer needed.
- virtual Result<ArrowArrayStream> Scan() = 0;
+ /// A builder with no options scans all columns of the current snapshot on
the main
+ /// branch. This table must outlive the builder.
+ virtual Result<std::unique_ptr<MetadataScanBuilder>> NewScan() const;
protected:
explicit MetadataTable(std::shared_ptr<Table> source_table);
- private:
- std::shared_ptr<Table> source_table_;
-};
-
-/// \brief Snapshot selection parameters for a time-travel scan.
-struct SnapshotSelection {
- /// \brief Select the current snapshot, a snapshot ID, or an as-of timestamp.
+ /// \brief Implement a scan that returns the columns of a projected schema.
///
- /// std::monostate selects the current snapshot.
- std::variant<std::monostate, int64_t, TimePointMs> snapshot;
+ /// \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;
- /// \brief Resolve the snapshot relative to this branch or tag.
+ /// \brief Implement a scan for the requested snapshot selection.
///
- /// An empty string uses the main branch.
- std::string ref_name;
+ /// 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;
+
+ private:
+ friend class MetadataScanBuilder;
+
+ std::shared_ptr<Table> source_table_;
};
/// \brief Base interface for metadata tables that support time travel.
///
-/// Keeping snapshot selection on this capability-specific interface prevents
it from
-/// being exposed by metadata tables that do not 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;
@@ -110,21 +116,20 @@ class ICEBERG_EXPORT TimeTravelMetadataTable : public
MetadataTable {
/// \brief Return true because this interface supports time travel.
bool supports_time_travel() const noexcept final;
- /// \brief Scan using the current snapshot on the main branch.
- Result<ArrowArrayStream> Scan() final;
-
- /// \brief Scan using the requested snapshot selection.
- ///
- /// \param snapshot_selection Snapshot ID, timestamp, and optional ref
selection.
- /// \return An Arrow stream containing the metadata table rows, or an error.
- Result<ArrowArrayStream> Scan(const SnapshotSelection& snapshot_selection);
-
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.
- virtual Result<ArrowArrayStream> ScanSnapshot(
- const SnapshotSelection& snapshot_selection) = 0;
+ ///
+ /// \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;
};
} // namespace iceberg
diff --git a/src/iceberg/inspect/metadata_table_columns_internal.h
b/src/iceberg/inspect/metadata_table_columns_internal.h
new file mode 100644
index 00000000..596ffa2f
--- /dev/null
+++ b/src/iceberg/inspect/metadata_table_columns_internal.h
@@ -0,0 +1,103 @@
+/*
+ * 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
+
+/// \file iceberg/inspect/metadata_table_columns_internal.h
+/// \brief Build only the selected columns of metadata table rows.
+
+#include <algorithm>
+#include <cstddef>
+#include <cstdint>
+#include <span>
+#include <vector>
+
+#include "iceberg/arrow_c_data.h"
+#include "iceberg/arrow_row_builder_internal.h"
+#include "iceberg/result.h"
+#include "iceberg/schema.h"
+#include "iceberg/schema_field.h"
+#include "iceberg/type.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg::internal {
+
+/// \brief Append the value of one metadata table column for a row.
+template <typename Row>
+using AppendColumnFn = Status (*)(ArrowArray* column, const Row& row);
+
+/// \brief A metadata table column and the function that appends its values.
+template <typename Row>
+struct MetadataTableColumn {
+ int32_t field_id;
+ AppendColumnFn<Row> append;
+};
+
+/// \brief Return the append functions for the columns of a projected schema.
+///
+/// \param schema The metadata table schema.
+/// \param projected_schema Projection of `schema` with the columns to return.
+/// \param columns The columns of `schema` with their append functions.
+/// \return One append function per column of `projected_schema`, in the same
order.
+/// Each projected field must select a whole column of `schema` by field id,
with the
+/// same type and without tightening its nullability.
+template <typename Row>
+Result<std::vector<AppendColumnFn<Row>>> SelectAppendFns(
+ const Schema& schema, const Schema& projected_schema,
+ std::span<const MetadataTableColumn<Row>> columns) {
+ std::vector<AppendColumnFn<Row>> append_fns;
+ append_fns.reserve(projected_schema.fields().size());
+ for (const auto& projected_field : projected_schema.fields()) {
+ // Only top-level columns have append functions, so nested field ids are
rejected.
+ ICEBERG_ASSIGN_OR_RAISE(auto field,
schema.GetFieldById(projected_field.field_id()));
+ if (!field.has_value()) {
+ return InvalidArgument("Unknown metadata column: {}",
projected_field.name());
+ }
+ if (*field->get().type() != *projected_field.type()) {
+ return NotSupported("Partial projection of metadata column is not
supported: {}",
+ projected_field.name());
+ }
+ if (field->get().optional() && !projected_field.optional()) {
+ return InvalidArgument(
+ "Nullable metadata column cannot be projected as required: {}",
+ projected_field.name());
+ }
+ auto column = std::ranges::find(columns, projected_field.field_id(),
+ &MetadataTableColumn<Row>::field_id);
+ if (column == columns.end()) {
+ return InvalidArgument("Metadata column has no append function: {}",
+ projected_field.name());
+ }
+ append_fns.push_back(column->append);
+ }
+ return append_fns;
+}
+
+/// \brief Append the selected columns of a row and finish the row.
+template <typename Row>
+Status AppendRow(ArrowRowBuilder& builder,
+ std::span<const AppendColumnFn<Row>> append_fns, const Row&
row) {
+ for (size_t index = 0; index < append_fns.size(); ++index) {
+ ICEBERG_RETURN_UNEXPECTED(
+ append_fns[index](builder.column(static_cast<int64_t>(index)), row));
+ }
+ return builder.FinishRow();
+}
+
+} // namespace iceberg::internal
diff --git a/src/iceberg/inspect/snapshots_table.cc
b/src/iceberg/inspect/snapshots_table.cc
index 9e9d7906..0dede12c 100644
--- a/src/iceberg/inspect/snapshots_table.cc
+++ b/src/iceberg/inspect/snapshots_table.cc
@@ -19,6 +19,7 @@
#include "iceberg/inspect/snapshots_table.h"
+#include <array>
#include <chrono>
#include <cstddef>
#include <memory>
@@ -30,6 +31,7 @@
#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/schema.h"
#include "iceberg/schema_field.h"
@@ -43,51 +45,66 @@
namespace iceberg {
namespace {
-Status AppendSnapshot(ArrowRowBuilder& builder, const Snapshot& snapshot) {
- ICEBERG_RETURN_UNEXPECTED(
- AppendInt(builder.column(0),
std::chrono::duration_cast<std::chrono::microseconds>(
-
snapshot.timestamp_ms.time_since_epoch())
- .count()));
- ICEBERG_RETURN_UNEXPECTED(AppendInt(builder.column(1),
snapshot.snapshot_id));
+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()) {
- ICEBERG_RETURN_UNEXPECTED(AppendInt(builder.column(2),
*snapshot.parent_snapshot_id));
- } else {
- ICEBERG_RETURN_UNEXPECTED(AppendNull(builder.column(2)));
+ 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()) {
- ICEBERG_RETURN_UNEXPECTED(AppendString(builder.column(3), *operation));
- } else {
- ICEBERG_RETURN_UNEXPECTED(AppendNull(builder.column(3)));
+ return AppendString(column, *operation);
}
+ return AppendNull(column);
+}
- ICEBERG_RETURN_UNEXPECTED(AppendString(builder.column(4),
snapshot.manifest_list));
+Status AppendManifestList(ArrowArray* column, const Snapshot& snapshot) {
+ return AppendString(column, snapshot.manifest_list);
+}
- if (!snapshot.summary.empty()) {
- if (snapshot.summary.contains(SnapshotSummaryFields::kOperation)) {
- auto summary = snapshot.summary;
- summary.erase(SnapshotSummaryFields::kOperation);
- ICEBERG_RETURN_UNEXPECTED(AppendStringMap(builder.column(5), summary));
- } else {
- ICEBERG_RETURN_UNEXPECTED(AppendStringMap(builder.column(5),
snapshot.summary));
- }
- } else {
- ICEBERG_RETURN_UNEXPECTED(AppendNull(builder.column(5)));
+Status AppendSummary(ArrowArray* column, const Snapshot& snapshot) {
+ if (snapshot.summary.empty()) {
+ return AppendNull(column);
}
-
- return builder.FinishRow();
+ 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&
schema) {
+ 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(schema, &arrow_schema));
- return std::unique_ptr<SnapshotsTableStream>(
- new SnapshotsTableStream(std::move(metadata),
std::move(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;
@@ -116,7 +133,8 @@ class SnapshotsTableStream {
if (snapshot == nullptr) [[unlikely]] {
continue;
}
- ICEBERG_RETURN_UNEXPECTED(AppendSnapshot(builder, *snapshot));
+ ICEBERG_RETURN_UNEXPECTED(
+ internal::AppendRow<Snapshot>(builder, append_fns_, *snapshot));
}
if (builder.num_rows() == 0) {
return std::nullopt;
@@ -138,10 +156,14 @@ class SnapshotsTableStream {
private:
SnapshotsTableStream(std::shared_ptr<const TableMetadata> metadata,
+ std::vector<internal::AppendColumnFn<Snapshot>>
append_fns,
ArrowSchema arrow_schema)
- : metadata_(std::move(metadata)), arrow_schema_(std::move(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;
};
@@ -173,9 +195,14 @@ Result<std::unique_ptr<SnapshotsTable>>
SnapshotsTable::Make(
return std::unique_ptr<SnapshotsTable>(new SnapshotsTable(std::move(table)));
}
-Result<ArrowArrayStream> SnapshotsTable::Scan() {
+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(),
*schema()));
+ auto stream, SnapshotsTableStream::Make(source_table()->metadata(),
+ projected_schema,
std::move(append_fns)));
return MakeArrowArrayStream(std::move(stream));
}
diff --git a/src/iceberg/inspect/snapshots_table.h
b/src/iceberg/inspect/snapshots_table.h
index d2f0ddf9..ae1434a5 100644
--- a/src/iceberg/inspect/snapshots_table.h
+++ b/src/iceberg/inspect/snapshots_table.h
@@ -42,10 +42,11 @@ class ICEBERG_EXPORT SnapshotsTable : public MetadataTable {
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> Scan() override;
+ Result<ArrowArrayStream> ScanProjection(const Schema& projected_schema)
const override;
private:
explicit SnapshotsTable(std::shared_ptr<Table> table);
diff --git a/src/iceberg/test/metadata_table_test.cc
b/src/iceberg/test/metadata_table_test.cc
index b014ef96..187646c9 100644
--- a/src/iceberg/test/metadata_table_test.cc
+++ b/src/iceberg/test/metadata_table_test.cc
@@ -19,6 +19,16 @@
#include "iceberg/inspect/metadata_table.h"
+#include <cstdint>
+#include <memory>
+#include <string>
+#include <utility>
+#include <variant>
+#include <vector>
+
+#include <arrow/c/bridge.h>
+#include <arrow/record_batch.h>
+#include <arrow/type.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
@@ -34,8 +44,46 @@
#include "iceberg/test/mock_catalog.h"
#include "iceberg/test/mock_io.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 {
+ public:
+ explicit RecordingTimeTravelTable(std::shared_ptr<Table> table)
+ : TimeTravelMetadataTable(std::move(table)) {}
+
+ 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");
+ }
+
+ private:
+ mutable SnapshotSelection scanned_snapshot_;
+ mutable std::vector<std::string> scanned_columns_;
+};
+
+} // namespace
class MetadataTableTest : public ::testing::Test {
protected:
@@ -56,6 +104,18 @@ class MetadataTableTest : public ::testing::Test {
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);
+ if (!reader_result.ok()) {
+ if (stream.release != nullptr) {
+ stream.release(&stream);
+ }
+ return InvalidArrowData(reader_result.status().ToString());
+ }
+ return reader_result.ValueUnsafe()->schema();
+ }
+
std::shared_ptr<Table> table_;
};
@@ -65,6 +125,51 @@ TEST_F(MetadataTableTest, FactoryRejectsNullSourceTable) {
EXPECT_THAT(result, HasErrorMessage("Table cannot be null"));
}
+TEST_F(MetadataTableTest, ScanBuilderSelectResolvesNamesImmediately) {
+ ICEBERG_UNWRAP_OR_FAIL(auto snapshots_table,
+ MetadataTable::Make<SnapshotsTable>(table_));
+
+ // 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 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());
+ ICEBERG_UNWRAP_OR_FAIL(auto insensitive_schema,
+ ReadSchema(std::move(insensitive_stream)));
+ EXPECT_THAT(insensitive_schema->field_names(),
::testing::ElementsAre("snapshot_id"));
+
+ // An invalid selection is reported when the scan is built.
+ ICEBERG_UNWRAP_OR_FAIL(auto invalid_builder, snapshots_table->NewScan());
+ auto invalid = invalid_builder->Select({"summary.key"}).Build();
+ EXPECT_THAT(invalid, IsError(ErrorKind::kValidationFailed));
+ EXPECT_THAT(invalid, HasErrorMessage("Cannot project Map without value
field"));
+}
+
+TEST_F(MetadataTableTest, ScanBuilderProject) {
+ ICEBERG_UNWRAP_OR_FAIL(auto snapshots_table,
+ MetadataTable::Make<SnapshotsTable>(table_));
+ auto projection = std::make_shared<Schema>(
+ std::vector<SchemaField>{SchemaField::MakeOptional(4, "operation",
string()),
+ SchemaField::MakeRequired(2, "snapshot_id",
int64())});
+
+ // 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 schema, ReadSchema(std::move(stream)));
+ EXPECT_THAT(schema->field_names(), ::testing::ElementsAre("operation",
"snapshot_id"));
+
+ ICEBERG_UNWRAP_OR_FAIL(auto null_builder, snapshots_table->NewScan());
+ EXPECT_THAT(null_builder->Project(nullptr).Build(),
+ IsError(ErrorKind::kInvalidArgument));
+}
+
TEST_F(MetadataTableTest, SupportsTimeTravel) {
ICEBERG_UNWRAP_OR_FAIL(auto snapshots_table,
MetadataTable::Make<SnapshotsTable>(table_));
@@ -72,6 +177,72 @@ TEST_F(MetadataTableTest, SupportsTimeTravel) {
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 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());
+ 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());
+ EXPECT_THAT(empty_ref_builder->UseRef("").Build(),
+ IsError(ErrorKind::kValidationFailed));
+}
+
+TEST_F(MetadataTableTest, HistoryTableScanIsNotSupported) {
+ 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));
+
+ // Tables without time travel reject snapshot selections.
+ ICEBERG_UNWRAP_OR_FAIL(auto snapshot_builder, history_table->NewScan());
+ auto snapshot_result = snapshot_builder->UseSnapshot(1).Build();
+ EXPECT_THAT(snapshot_result, IsError(ErrorKind::kNotSupported));
+ EXPECT_THAT(snapshot_result, HasErrorMessage("does not support time
travel"));
}
} // namespace iceberg
diff --git a/src/iceberg/test/metadata_table_test_base.h
b/src/iceberg/test/metadata_table_test_base.h
index b9f601cc..cefdb5cb 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_table.h"
#include "iceberg/schema.h"
#include "iceberg/schema_field.h"
#include "iceberg/schema_internal.h"
@@ -80,7 +81,21 @@ class MetadataTableTestBase : public ::testing::Test {
"s3://bucket/meta.json", io_,
catalog_));
}
- /// \brief Import and consume a Scan()-produced ArrowArrayStream.
+ /// \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();
+ }
+
+ /// \brief Scan the selected columns of a metadata table.
+ static Result<ArrowArrayStream> ScanColumns(MetadataTable& table,
+ 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();
+ }
+
+ /// \brief Import and consume a scan-produced ArrowArrayStream.
static Result<std::vector<std::shared_ptr<::arrow::RecordBatch>>>
ReadAllBatches(
ArrowArrayStream&& stream) {
auto reader_result = ::arrow::ImportRecordBatchReader(&stream);
diff --git a/src/iceberg/test/snapshots_table_test.cc
b/src/iceberg/test/snapshots_table_test.cc
index cddfce69..9a62c6b6 100644
--- a/src/iceberg/test/snapshots_table_test.cc
+++ b/src/iceberg/test/snapshots_table_test.cc
@@ -77,7 +77,7 @@ TEST_F(SnapshotsTableTest, Construct) {
TEST_F(SnapshotsTableTest, Scan) {
// Scan the snapshots table once and verify all columns of the result.
- ICEBERG_UNWRAP_OR_FAIL(auto stream, snapshots_table_->Scan());
+ ICEBERG_UNWRAP_OR_FAIL(auto stream, Scan(*snapshots_table_));
ICEBERG_UNWRAP_OR_FAIL(auto batches, ReadAllBatches(std::move(stream)));
ASSERT_EQ(batches.size(), 1);
const auto& batch = batches.front();
@@ -132,6 +132,164 @@ TEST_F(SnapshotsTableTest, Scan) {
EXPECT_THAT(second_summary,
::testing::Contains(::testing::Pair("total-records", "2")));
}
+TEST_F(SnapshotsTableTest, ScanSelectedColumns) {
+ ICEBERG_UNWRAP_OR_FAIL(auto stream,
+ ScanColumns(*snapshots_table_, {"operation",
"snapshot_id"}));
+ ICEBERG_UNWRAP_OR_FAIL(auto batches, ReadAllBatches(std::move(stream)));
+ ASSERT_EQ(batches.size(), 1);
+ const auto& batch = batches.front();
+ EXPECT_EQ(batch->num_rows(), 2);
+ ASSERT_EQ(batch->num_columns(), 2);
+ EXPECT_EQ(batch->column_name(0), "snapshot_id");
+ EXPECT_EQ(batch->column_name(1), "operation");
+ ASSERT_EQ(batch->column(0)->type_id(), ::arrow::Type::INT64);
+ ASSERT_EQ(batch->column(1)->type_id(), ::arrow::Type::STRING);
+
+ auto snapshot_ids =
std::static_pointer_cast<::arrow::Int64Array>(batch->column(0));
+ EXPECT_EQ(snapshot_ids->Value(0), 1);
+ EXPECT_EQ(snapshot_ids->Value(1), 2);
+ auto operations =
std::static_pointer_cast<::arrow::StringArray>(batch->column(1));
+ EXPECT_EQ(operations->GetString(0), "append");
+ EXPECT_EQ(operations->GetString(1), "append");
+}
+
+TEST_F(SnapshotsTableTest, ScanSelectedColumnsIgnoresUnknownNames) {
+ ICEBERG_UNWRAP_OR_FAIL(auto stream,
+ ScanColumns(*snapshots_table_, {"parent_id",
"missing"}));
+ ICEBERG_UNWRAP_OR_FAIL(auto batches, ReadAllBatches(std::move(stream)));
+ ASSERT_EQ(batches.size(), 1);
+ ASSERT_EQ(batches.front()->num_columns(), 1);
+ EXPECT_EQ(batches.front()->column_name(0), "parent_id");
+}
+
+TEST_F(SnapshotsTableTest, ScanSelectedColumnsCaseSensitivity) {
+ ICEBERG_UNWRAP_OR_FAIL(
+ auto insensitive_stream,
+ ScanColumns(*snapshots_table_, {"SNAPSHOT_ID"},
/*case_sensitive=*/false));
+ ICEBERG_UNWRAP_OR_FAIL(auto insensitive_batches,
+ ReadAllBatches(std::move(insensitive_stream)));
+ ASSERT_EQ(insensitive_batches.size(), 1);
+ ASSERT_EQ(insensitive_batches.front()->num_columns(), 1);
+ EXPECT_EQ(insensitive_batches.front()->column_name(0), "snapshot_id");
+
+ ICEBERG_UNWRAP_OR_FAIL(auto sensitive_stream,
+ ScanColumns(*snapshots_table_, {"SNAPSHOT_ID"}));
+ ICEBERG_UNWRAP_OR_FAIL(auto sensitive_batches,
+ ReadAllBatches(std::move(sensitive_stream)));
+ ASSERT_EQ(sensitive_batches.size(), 1);
+ EXPECT_EQ(sensitive_batches.front()->num_columns(), 0);
+}
+
+TEST_F(SnapshotsTableTest, ScanAllColumnsSelection) {
+ // Selecting `*` returns the same columns as a scan without a selection.
+ ICEBERG_UNWRAP_OR_FAIL(auto stream, ScanColumns(*snapshots_table_, {"*"}));
+ ICEBERG_UNWRAP_OR_FAIL(auto batches, ReadAllBatches(std::move(stream)));
+ ASSERT_EQ(batches.size(), 1);
+ EXPECT_EQ(batches.front()->num_rows(), 2);
+ EXPECT_EQ(batches.front()->num_columns(), 6);
+}
+
+TEST_F(SnapshotsTableTest, ScanBuildsRepeatedly) {
+ // A builder can be built more than once; each build returns a new stream.
+ 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 batches, ReadAllBatches(std::move(stream)));
+ ASSERT_EQ(batches.size(), 1);
+ EXPECT_EQ(batches.front()->num_rows(), 2);
+ ASSERT_EQ(batches.front()->num_columns(), 1);
+ EXPECT_EQ(batches.front()->column_name(0), "snapshot_id");
+ }
+}
+
+TEST_F(SnapshotsTableTest, ScanRejectsTimeTravel) {
+ ICEBERG_UNWRAP_OR_FAIL(auto builder, snapshots_table_->NewScan());
+ EXPECT_THAT(builder->UseSnapshot(1).Build(),
IsError(ErrorKind::kNotSupported));
+
+ ICEBERG_UNWRAP_OR_FAIL(auto ref_builder, snapshots_table_->NewScan());
+ EXPECT_THAT(ref_builder->UseRef("dev").Build(),
IsError(ErrorKind::kNotSupported));
+
+ ICEBERG_UNWRAP_OR_FAIL(auto time_builder, snapshots_table_->NewScan());
+ EXPECT_THAT(time_builder->AsOfTime(1).Build(),
IsError(ErrorKind::kNotSupported));
+}
+
+TEST_F(SnapshotsTableTest, ScanProjectedColumns) {
+ // Project() reorders columns and keeps the whole summary map.
+ auto projection = std::make_shared<Schema>(std::vector<SchemaField>{
+ SchemaField::MakeOptional(
+ 6, "summary",
+ std::make_shared<MapType>(SchemaField::MakeRequired(7, "key",
string()),
+ 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 batches, ReadAllBatches(std::move(stream)));
+ ASSERT_EQ(batches.size(), 1);
+ const auto& batch = batches.front();
+ EXPECT_EQ(batch->num_rows(), 2);
+ ASSERT_EQ(batch->num_columns(), 2);
+ EXPECT_EQ(batch->column_name(0), "summary");
+ EXPECT_EQ(batch->column_name(1), "snapshot_id");
+ auto summaries =
std::static_pointer_cast<::arrow::MapArray>(batch->column(0));
+ EXPECT_THAT(GetMapEntries(summaries, 1),
+ ::testing::Contains(::testing::Pair("total-records", "2")));
+ auto snapshot_ids =
std::static_pointer_cast<::arrow::Int64Array>(batch->column(1));
+ EXPECT_EQ(snapshot_ids->Value(0), 1);
+ EXPECT_EQ(snapshot_ids->Value(1), 2);
+}
+
+TEST_F(SnapshotsTableTest, ScanRejectsInvalidProjection) {
+ // An optional column cannot be projected as required.
+ auto required_parent = std::make_shared<Schema>(
+ std::vector<SchemaField>{SchemaField::MakeRequired(3, "parent_id",
int64())});
+ ICEBERG_UNWRAP_OR_FAIL(auto builder, snapshots_table_->NewScan());
+ EXPECT_THAT(builder->Project(required_parent).Build(),
+ IsError(ErrorKind::kInvalidArgument));
+
+ // A column must keep its type.
+ auto string_id = std::make_shared<Schema>(
+ std::vector<SchemaField>{SchemaField::MakeRequired(2, "snapshot_id",
string())});
+ ICEBERG_UNWRAP_OR_FAIL(auto type_builder, snapshots_table_->NewScan());
+ EXPECT_THAT(type_builder->Project(string_id).Build(),
+ IsError(ErrorKind::kNotSupported));
+
+ // Nested fields are not top-level columns.
+ auto summary_key = std::make_shared<Schema>(
+ std::vector<SchemaField>{SchemaField::MakeRequired(7, "key", string())});
+ ICEBERG_UNWRAP_OR_FAIL(auto nested_builder, snapshots_table_->NewScan());
+ EXPECT_THAT(nested_builder->Project(summary_key).Build(),
+ IsError(ErrorKind::kInvalidArgument));
+}
+
+TEST_F(SnapshotsTableTest, ScanNoColumns) {
+ ICEBERG_UNWRAP_OR_FAIL(auto stream, ScanColumns(*snapshots_table_, {}));
+ ICEBERG_UNWRAP_OR_FAIL(auto batches, ReadAllBatches(std::move(stream)));
+ ASSERT_EQ(batches.size(), 1);
+ EXPECT_EQ(batches.front()->num_rows(), 2);
+ EXPECT_EQ(batches.front()->num_columns(), 0);
+}
+
+TEST_F(SnapshotsTableTest, ScanSelectedMapColumn) {
+ ICEBERG_UNWRAP_OR_FAIL(auto stream, ScanColumns(*snapshots_table_,
{"summary.value"}));
+ ICEBERG_UNWRAP_OR_FAIL(auto batches, ReadAllBatches(std::move(stream)));
+ ASSERT_EQ(batches.size(), 1);
+ const auto& batch = batches.front();
+ ASSERT_EQ(batch->num_columns(), 1);
+ EXPECT_EQ(batch->column_name(0), "summary");
+ ASSERT_EQ(batch->column(0)->type_id(), ::arrow::Type::MAP);
+ auto summaries =
std::static_pointer_cast<::arrow::MapArray>(batch->column(0));
+ EXPECT_THAT(GetMapEntries(summaries, 0),
+ ::testing::Contains(::testing::Pair("total-records", "1")));
+ EXPECT_THAT(GetMapEntries(summaries, 1),
+ ::testing::Contains(::testing::Pair("total-records", "2")));
+
+ // A map cannot be projected without its values.
+ ICEBERG_UNWRAP_OR_FAIL(auto builder, snapshots_table_->NewScan());
+ EXPECT_THAT(builder->Select({"summary.key"}).Build(),
+ IsError(ErrorKind::kValidationFailed));
+}
+
TEST_F(SnapshotsTableTest, ScanEmptySnapshotList) {
// A table with zero snapshots should return zero rows.
ICEBERG_UNWRAP_OR_FAIL(
@@ -141,7 +299,7 @@ TEST_F(SnapshotsTableTest, ScanEmptySnapshotList) {
ICEBERG_UNWRAP_OR_FAIL(snapshots_table_,
MetadataTable::Make<SnapshotsTable>(empty_table));
- ICEBERG_UNWRAP_OR_FAIL(auto stream, snapshots_table_->Scan());
+ ICEBERG_UNWRAP_OR_FAIL(auto stream, Scan(*snapshots_table_));
ICEBERG_UNWRAP_OR_FAIL(auto batches, ReadAllBatches(std::move(stream)));
EXPECT_TRUE(batches.empty());
}
@@ -153,7 +311,7 @@ TEST_F(SnapshotsTableTest, ScanSkipsNullSnapshots) {
ICEBERG_UNWRAP_OR_FAIL(auto snapshots_table,
MetadataTable::Make<SnapshotsTable>(table));
- ICEBERG_UNWRAP_OR_FAIL(auto stream, snapshots_table->Scan());
+ ICEBERG_UNWRAP_OR_FAIL(auto stream, Scan(*snapshots_table));
ICEBERG_UNWRAP_OR_FAIL(auto batches, ReadAllBatches(std::move(stream)));
ASSERT_EQ(batches.size(), 1);
EXPECT_EQ(batches.front()->num_rows(), 2);
@@ -170,7 +328,7 @@ TEST_F(SnapshotsTableTest, ScanEmptySummary) {
ICEBERG_UNWRAP_OR_FAIL(auto snapshots_table,
MetadataTable::Make<SnapshotsTable>(table));
- ICEBERG_UNWRAP_OR_FAIL(auto stream, snapshots_table->Scan());
+ ICEBERG_UNWRAP_OR_FAIL(auto stream, Scan(*snapshots_table));
ICEBERG_UNWRAP_OR_FAIL(auto batches, ReadAllBatches(std::move(stream)));
ASSERT_EQ(batches.size(), 1);
auto summaries =
@@ -189,7 +347,7 @@ TEST_F(SnapshotsTableTest, ScanReturnsMultipleBatches) {
ICEBERG_UNWRAP_OR_FAIL(auto snapshots_table,
MetadataTable::Make<SnapshotsTable>(table));
- ICEBERG_UNWRAP_OR_FAIL(auto stream, snapshots_table->Scan());
+ ICEBERG_UNWRAP_OR_FAIL(auto 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);
@@ -205,7 +363,7 @@ TEST_F(SnapshotsTableTest,
ScanUsesMetadataCapturedAtCreation) {
ICEBERG_UNWRAP_OR_FAIL(auto snapshots_table,
MetadataTable::Make<SnapshotsTable>(table));
- ICEBERG_UNWRAP_OR_FAIL(auto stream, snapshots_table->Scan());
+ ICEBERG_UNWRAP_OR_FAIL(auto stream, Scan(*snapshots_table));
auto reader_result = ::arrow::ImportRecordBatchReader(&stream);
ASSERT_TRUE(reader_result.ok()) << reader_result.status().ToString();
auto reader = std::move(reader_result).ValueUnsafe();
diff --git a/src/iceberg/type_fwd.h b/src/iceberg/type_fwd.h
index 477774a1..9386c9dd 100644
--- a/src/iceberg/type_fwd.h
+++ b/src/iceberg/type_fwd.h
@@ -281,6 +281,7 @@ class PositionDeleteIndex;
/// \brief Metadata tables.
class HistoryTable;
+class MetadataScanBuilder;
class MetadataTable;
class SnapshotsTable;