feat(inspect): implement streaming SnapshotsTable scans - #801
Conversation
28c4513 to
a53f8ce
Compare
9bc23e1 to
04b657f
Compare
- Add Scan() virtual method and Scan() convenience overload to MetadataTable - Add SnapshotSelection struct for time-travel snapshot resolution - Add supports_time_travel() concrete method driven by kind() - Implement SnapshotsTable::Scan() to materialize snapshot rows via ArrowRowBuilder
There was a problem hiding this comment.
Pull request overview
Implements initial metadata-table scanning support by adding a Scan() API to MetadataTable (with snapshot-selection parameters for future time-travel) and providing a concrete SnapshotsTable::Scan() implementation that materializes snapshot rows into Arrow arrays. The PR also restructures/extends the metadata-table test suite to validate schemas and snapshot scanning behavior.
Changes:
- Added
SnapshotSelectionand a virtualMetadataTable::Scan()API (with a convenience overload) plussupports_time_travel(). - Implemented
SnapshotsTable::Scan()to emit snapshot rows (6 columns) viaArrowRowBuilder. - Added/expanded tests and wired new test sources into the metadata-table test target.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/iceberg/inspect/metadata_table.h | Adds SnapshotSelection, Scan() API, and supports_time_travel() declaration/docs. |
| src/iceberg/inspect/metadata_table.cc | Implements default Scan() + supports_time_travel() and wires factory for kinds. |
| src/iceberg/inspect/snapshots_table.h | Declares SnapshotsTable::Scan() override. |
| src/iceberg/inspect/snapshots_table.cc | Implements snapshot scanning into Arrow via ArrowRowBuilder. |
| src/iceberg/test/metadata_table_test.cc | Simplifies base setup and adds SupportsTimeTravel test. |
| src/iceberg/test/metadata_table_test_base.h | New shared fixture/helpers for metadata table tests. |
| src/iceberg/test/snapshots_table_test.cc | New tests validating snapshots table construction/schema/scan output. |
| src/iceberg/test/history_table_test.cc | New schema test for history table. |
| src/iceberg/test/CMakeLists.txt | Adds new test sources to the metadata-table test target. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
src/iceberg/inspect/metadata_table.cc:38
supports_time_travel()is currently hard-coded to return false. That matches today’s twoKindvalues, but it’s easy to forget to update once additional metadata table kinds are added, and it doesn’t reflect the docstring/PR description that this is kind-driven. Consider switching onkind()and making the non-exhaustive case unreachable to keep future additions honest.
bool MetadataTable::supports_time_travel() const noexcept { return false; }
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2cb7351 to
dfd5e21
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.
Suppressed comments (3)
src/iceberg/inspect/metadata_table.h:80
- The PR description says
Scan()returnsResult<ArrowArray>with a default NotSupported implementation plus a zero-arg overload, but the code introducesResult<ArrowArrayStream>as a pure virtual method onMetadataTable(and adds a separateTimeTravelMetadataTable). Either the PR description should be updated to match the implemented API, or the API should be adjusted to match the stated intent (including explicitly providing a default NotSupported if that’s still desired).
/// \brief Scan the metadata table without time travel.
///
/// The caller owns the returned stream and must release it with
/// ArrowArrayStreamRelease.
virtual Result<ArrowArrayStream> Scan() = 0;
src/iceberg/inspect/snapshots_table.cc:73
- This copies
snapshot.summaryfor every scanned snapshot solely to removeoperation. For large summaries or large snapshot counts, this adds avoidable per-row allocations and hashing work. Prefer avoiding the full copy (e.g., detect theoperation-only case without copying, or build a filtered map only when needed, or extend the map-appending helper to accept a predicate/skip key).
auto summary = snapshot.summary;
summary.erase(SnapshotSummaryFields::kOperation);
if (summary.empty()) {
ICEBERG_RETURN_UNEXPECTED(AppendNull(builder.column(5)));
} else {
ICEBERG_RETURN_UNEXPECTED(AppendStringMap(builder.column(5), summary));
}
src/iceberg/test/snapshots_table_test.cc:112
- These constants rely on integer-literal type promotion rules. For clarity and to avoid accidental narrowing/overflow on some toolchains, consider making the intent explicit with
int64_t/LLliterals (e.g.,1234567890000LL * 1000).
EXPECT_EQ(committed_at->Value(0), 1234567890000 * 1000);
EXPECT_EQ(committed_at->Value(1), 9876543210000 * 1000);
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.
Suppressed comments (3)
src/iceberg/inspect/snapshots_table.cc:73
- This makes a full copy of
snapshot.summaryfor every row just to drop theoperationentry, which can be costly for large scans. A more efficient approach is to build the Arrow map by iteratingsnapshot.summaryand appending only entries whose key !=kOperation, avoiding the intermediate copy (or only copying when the operation key is actually present).
auto summary = snapshot.summary;
summary.erase(SnapshotSummaryFields::kOperation);
if (summary.empty()) {
ICEBERG_RETURN_UNEXPECTED(AppendNull(builder.column(5)));
} else {
ICEBERG_RETURN_UNEXPECTED(AppendStringMap(builder.column(5), summary));
}
src/iceberg/inspect/metadata_table.h:60
- The public factory now only supports compile-time selection (
Make<ConcreteType>(...)). This makes it harder/impossible for callers who only have aMetadataTable::Kindat runtime to construct a table. Consider keeping (or reintroducing) aMake(table, Kind)overload as a thin wrapper around the concreteMake()methods to preserve the runtime-factory use case.
template <typename MetadataTableType>
requires std::derived_from<MetadataTableType, MetadataTable>
static Result<std::unique_ptr<MetadataTableType>> Make(std::shared_ptr<Table> table) {
return MetadataTableType::Make(std::move(table));
}
src/iceberg/inspect/metadata_table.h:80
- The PR description says
Scan()returnsResult<ArrowArray>, but the implementation exposesResult<ArrowArrayStream>. Please update the PR description (or the API) so the documented behavior matches the code.
/// \brief Scan the metadata table without time travel.
///
/// The caller owns the returned stream and must release it with
/// ArrowArrayStreamRelease.
virtual Result<ArrowArrayStream> Scan() = 0;
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.
Suppressed comments (5)
src/iceberg/test/metadata_table_test_base.h:88
- On the
ImportRecordBatchReaderfailure path, the passed-inArrowArrayStreamis not released, which can leak stream-owned resources (and the new scan API explicitly documents that callers own the stream). SinceReadAllBatchestakes ownership via rvalue and returns early here, it should callArrowArrayStreamRelease(&stream)(or equivalent helper) before returning the error.
static Result<std::vector<std::shared_ptr<::arrow::RecordBatch>>> ReadAllBatches(
ArrowArrayStream&& stream) {
auto reader_result = ::arrow::ImportRecordBatchReader(&stream);
if (!reader_result.ok()) {
return InvalidArrowData(reader_result.status().ToString());
}
src/iceberg/inspect/snapshots_table.cc:102
Next()dereferencestable_unconditionally. AfterClose()runs,table_is reset, and any subsequentNext()call would crash. Even if Arrow consumers are expected not to callget_nextafterrelease, adding an explicit guard (e.g., return an error or end-of-stream whentable_ == nullptr) makes the stream more robust and prevents hard-to-diagnose crashes in misuse scenarios.
Result<std::optional<ArrowArray>> Next() {
const auto& snapshots = table_->snapshots();
if (next_snapshot_ == snapshots.size()) {
return std::nullopt;
}
src/iceberg/inspect/snapshots_table.cc:73
- This copies the entire
snapshot.summarymap for every emitted row, which adds allocation/copy overhead during scans (especially with large summaries and across batches). A more efficient approach is to avoid the full copy by iterating the original map and appending only non-kOperationentries (or building a filtered view/temporary only when the operation key is present).
auto summary = snapshot.summary;
summary.erase(SnapshotSummaryFields::kOperation);
if (summary.empty()) {
ICEBERG_RETURN_UNEXPECTED(AppendNull(builder.column(5)));
} else {
ICEBERG_RETURN_UNEXPECTED(AppendStringMap(builder.column(5), summary));
}
src/iceberg/test/metadata_table_test_base.h:109
- Tests hardcode the "operation" summary key as a string literal here, while other new tests use
SnapshotSummaryFields::kOperation. Using the shared constant consistently would reduce duplication and prevent future drift if the canonical key changes.
{"operation", "append"},
src/iceberg/inspect/history_table.cc:55
MetadataTablenow requiresScan()as the primary non-time-travel entry point, butHistoryTableis constructible and yet always returnsNotSupportedforScan(). This makes the interface surprising for consumers (a table type that cannot be scanned). Consider either implementing a scan forHistoryTable, moving it behind a capability-specific interface (similar toTimeTravelMetadataTable), or making the non-scannable type unconstructible/hidden until scan support exists.
Result<ArrowArrayStream> HistoryTable::Scan() {
return NotSupported("Scan is not supported for the history table");
}
| /// | ||
| /// \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); |
There was a problem hiding this comment.
Is it better to remove this extra layer of TimeTravelMetadataTable? We can check supports_time_travel() and by default Scan(const SnapshotSelection& snapshot_selection) can just return NotSupported.
| /// \brief Scan the metadata table without time travel. | ||
| /// | ||
| /// The caller owns the returned stream and must release it with | ||
| /// ArrowArrayStreamRelease. |
There was a problem hiding this comment.
ArrowArrayStreamRelease is not declared by this repository's Arrow C Data API; callers must invoke the stream's release callback. Please correct the ownership documentation.
| } | ||
|
|
||
| Result<std::optional<ArrowArray>> Next() { | ||
| const auto& snapshots = table_->snapshots(); |
There was a problem hiding this comment.
This stream re-reads the mutable Table on every batch. If Table::Refresh() runs between get_next calls, later batches can come from a different snapshot list, skipping or truncating rows. Please capture the metadata or snapshot list when Scan() creates the stream.
| return std::unique_ptr<HistoryTable>(new HistoryTable(std::move(table))); | ||
| } | ||
|
|
||
| Result<ArrowArrayStream> HistoryTable::Scan() { |
There was a problem hiding this comment.
HistoryTable remains constructible, but the new common Scan() API always returns NotSupported here. Java HistoryTable scans table.history() into rows. Please implement a streaming history scan, or keep non-scannable tables out of this interface.
| new SnapshotsTableStream(std::move(table), std::move(arrow_schema))); | ||
| } | ||
|
|
||
| ~SnapshotsTableStream() { std::ignore = Close(); } |
There was a problem hiding this comment.
std::ignore is used here without a direct include. Please add or replace std::ignore so this file does not rely on transitive includes.
Summary
Implement streaming scans for
SnapshotsTableand refine the metadata-table scan APIs so snapshot metadata can be consumed safely in bounded Arrow batches.Changes
Metadata table APIs
MetadataTable::Make<T>()factory that preserves the concrete metadata-table type.MetadataTableinterface limited to non-time-travelScan()calls.TimeTravelMetadataTableas the capability-specific interface for scans usingSnapshotSelection.std::variant<std::monostate, int64_t, TimePointMs>.ArrowArrayStreamfrom scan APIs and document stream ownership.Snapshots table
SnapshotsTable::Scan()using a stateful Arrow stream.ArrowSchemaonce when creating the stream and release owned resources when the stream closes.MetadataTable::kBatchSizerows per batch.committed_at,snapshot_id,parent_id,operation,manifest_list, andsummaryusingArrowRowBuilder.SnapshotSummaryFields::kOperationfrom the summary map and emit null when the remaining summary is empty.Arrow row builder
ArrowRowBuilder::num_rows()so streaming producers can enforce batch-size limits without maintaining duplicate row counters.Tests
Testing