feat(inspect): implement streaming SnapshotsTable scans - #801
Conversation
28c4513 to
a53f8ce
Compare
9bc23e1 to
04b657f
Compare
efc76b8 to
3b11911
Compare
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; }
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 documented (and described in the PR) as being driven by the metadata table kind, but the implementation always returns false regardless of kind. This makes the API misleading and forces future kinds to remember to update callers rather than the method itself.
bool MetadataTable::supports_time_travel() const noexcept { return false; }
| /// | ||
| /// The caller owns the returned stream and must invoke its `release` callback | ||
| /// when the stream is no longer needed. | ||
| virtual Result<ArrowArrayStream> Scan() = 0; |
There was a problem hiding this comment.
It would be better to decouple with Arrow types.
There was a problem hiding this comment.
Agreed. Decoupling the metadata table API from Arrow C types would be cleaner. This likely deserves a metadata-specific batch reader abstraction rather than reusing the file-oriented Reader interface. Since that is a broader API change, I would prefer to address it in a follow-up PR and keep ArrowArrayStream for this PR.
There was a problem hiding this comment.
Well, this defines the basic interface for all metadata tables. I think it will be better to get it right first, maybe in a separate PR. @wgtmac What do you think?
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Pull request overview
Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.
| const bool has_summary = !snapshot.summary.empty(); | ||
| auto summary = snapshot.summary; | ||
| summary.erase(SnapshotSummaryFields::kOperation); | ||
| if (!has_summary) { | ||
| ICEBERG_RETURN_UNEXPECTED(AppendNull(builder.column(5))); | ||
| } else { | ||
| ICEBERG_RETURN_UNEXPECTED(AppendStringMap(builder.column(5), summary)); |
| const bool has_summary = !snapshot.summary.empty(); | ||
| auto summary = snapshot.summary; | ||
| summary.erase(SnapshotSummaryFields::kOperation); | ||
| if (!has_summary) { | ||
| ICEBERG_RETURN_UNEXPECTED(AppendNull(builder.column(5))); | ||
| } else { | ||
| ICEBERG_RETURN_UNEXPECTED(AppendStringMap(builder.column(5), summary)); |
| const bool has_summary = !snapshot.summary.empty(); | ||
| auto summary = snapshot.summary; | ||
| summary.erase(SnapshotSummaryFields::kOperation); | ||
| if (!has_summary) { | ||
| ICEBERG_RETURN_UNEXPECTED(AppendNull(builder.column(5))); | ||
| } else { | ||
| ICEBERG_RETURN_UNEXPECTED(AppendStringMap(builder.column(5), summary)); | ||
| } |
| TEST_F(SnapshotsTableTest, ScanReturnsMultipleBatches) { | ||
| auto snapshot = MakeTestSnapshots().first; | ||
| std::vector<std::shared_ptr<Snapshot>> snapshots(1025, snapshot); | ||
| ICEBERG_UNWRAP_OR_FAIL(auto table, MakeTableWithSnapshots(std::move(snapshots), | ||
| /*current_snapshot_id=*/1)); | ||
| ICEBERG_UNWRAP_OR_FAIL(auto snapshots_table, | ||
| MetadataTable::Make<SnapshotsTable>(table)); | ||
|
|
||
| ICEBERG_UNWRAP_OR_FAIL(auto stream, snapshots_table->Scan()); | ||
| ICEBERG_UNWRAP_OR_FAIL(auto batches, ReadAllBatches(std::move(stream))); | ||
| ASSERT_EQ(batches.size(), 2); | ||
| EXPECT_EQ(batches[0]->num_rows(), 1024); | ||
| EXPECT_EQ(batches[1]->num_rows(), 1); | ||
| } |
There was a problem hiding this comment.
🟡 Changes recommended
The snapshots summary filtering behavior and its test expectations conflict with the PR’s stated contract (null vs empty summary after removing operation).
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 15/15 changed files
- Comments generated: 2
- Review effort level: Lite
| const bool has_summary = !snapshot.summary.empty(); | ||
| auto summary = snapshot.summary; | ||
| summary.erase(SnapshotSummaryFields::kOperation); | ||
| if (!has_summary) { | ||
| ICEBERG_RETURN_UNEXPECTED(AppendNull(builder.column(5))); | ||
| } else { | ||
| ICEBERG_RETURN_UNEXPECTED(AppendStringMap(builder.column(5), summary)); | ||
| } |
| EXPECT_TRUE(summaries->IsNull(0)); | ||
| EXPECT_FALSE(summaries->IsNull(1)); | ||
| EXPECT_EQ(summaries->value_length(1), 0); |
- 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
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🔵 Needs a closer look
The snapshots summary null-vs-empty-map behavior is inconsistent with the PR description (and should be aligned in code/tests before approval).
Review details
Suppressed comments (1)
src/iceberg/inspect/snapshots_table.cc:76
AppendSnapshot()decides whether to emit a nullsummaryvalue based onsnapshot.summary.empty()before removingSnapshotSummaryFields::kOperation. This means a snapshot whose summary only containsoperationwill currently emit a non-null but empty map, which contradicts the PR description (“emit null when the remaining summary is empty”). Consider checkingsummary.empty()after erasingkOperationand emitting null when the filtered map is empty (and updateSnapshotsTableTest.ScanEmptySummaryaccordingly).
const bool has_summary = !snapshot.summary.empty();
auto summary = snapshot.summary;
summary.erase(SnapshotSummaryFields::kOperation);
if (!has_summary) {
ICEBERG_RETURN_UNEXPECTED(AppendNull(builder.column(5)));
} else {
ICEBERG_RETURN_UNEXPECTED(AppendStringMap(builder.column(5), summary));
}
- Files reviewed: 15/15 changed files
- Comments generated: 0 new
- Review effort level: Lite
There was a problem hiding this comment.
🔵 Needs a closer look
The snapshots summary null/empty emission behavior in SnapshotsTable is inconsistent with the PR description and should be reconciled (code/tests/docs) before approval.
Review details
Suppressed comments (1)
src/iceberg/inspect/snapshots_table.cc:72
- The summary null/empty handling doesn’t match the PR description (“emit null when the remaining summary is empty”). Here
has_summaryis computed before removingSnapshotSummaryFields::kOperation, so a summary that only containsoperationbecomes an empty map but is still emitted as a non-null (empty) map. Decide on the intended semantics (null vs empty map after filtering) and make code + tests/description consistent (typically: eraseoperationfirst, then if the filtered map is empty append null).
const bool has_summary = !snapshot.summary.empty();
auto summary = snapshot.summary;
summary.erase(SnapshotSummaryFields::kOperation);
if (!has_summary) {
ICEBERG_RETURN_UNEXPECTED(AppendNull(builder.column(5)));
- Files reviewed: 14/14 changed files
- Comments generated: 0 new
- Review effort level: Lite
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