Skip to content

feat(inspect): support selecting columns in metadata table scans - #1008

Open
manuzhang wants to merge 6 commits into
apache:mainfrom
manuzhang:metadata-table-select-columns
Open

manuzhang wants to merge 6 commits into
apache:mainfrom
manuzhang:metadata-table-select-columns

Conversation

@manuzhang

@manuzhang manuzhang commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

What

  • Add ColumnSelection (column names plus case sensitivity), MetadataTable::ScanColumns(const ColumnSelection&), and TimeTravelMetadataTable::ScanColumns(const SnapshotSelection&, const ColumnSelection&). Names resolve through Schema::Select, as in TableScanBuilder::Select: nested names are dot-concatenated, columns come back in schema order, * selects all columns, and unknown names are ignored (as in Java's Schema.select). An empty list returns rows without columns, which is enough to count rows. Selecting a map's value returns the whole map; selecting only its key is an error.
  • Scan() is no longer virtual. Tables implement the protected ScanProjection(const Schema& projected_schema) instead, and ScanSnapshot also receives the projected schema.
  • Add inspect/metadata_table_columns_internal.h, which maps the projected columns to per-column append functions so a table builds only the selected columns. Selecting only some fields of a nested struct column returns NotSupported for now; no current metadata table has a struct column.
  • SnapshotsTable builds only the selected columns. HistoryTable scans still return NotSupported.

Why

Metadata table scans currently always return every column. Java's metadata tables support newScan().select(...): StaticDataTask projects each row to scan.schema(), so an engine running SELECT name FROM t.refs only gets name. Without selection, a C++ caller that needs one column still pays to build all of them, for example copying every snapshot's summary map.

Behavior change

Testing

Added metadata_table_test cases for SnapshotsTable covering selection order, unknown names, case sensitivity, * and no selection, an empty selection, and map columns (summary.value returns the whole map, summary.key is an error). A recording TimeTravelMetadataTable checks the snapshot selection and projected columns passed by Scan(), ScanColumns, Scan(const SnapshotSelection&), and Scan({}), and another case checks that HistoryTable::ScanColumns returns NotSupported.

🤖 Generated with Claude Code

Add ColumnSelection and Scan overloads that return only the requested
columns, matching Java's metadata table select(). Names resolve through
Schema::Select, so nested names, case sensitivity, `*`, and unknown names
behave as in TableScanBuilder::Select.

Tables now implement ScanProjection(const Schema&) and build only the
selected columns. SnapshotsTable uses a per-column append table from the
new metadata_table_columns_internal.h helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 13, 2026 15:42

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot review overview

2 open findings
What changed in this PR

Adds column projection support to C++ Iceberg metadata table scans (including time travel), aligning behavior with Java (newScan().select(...)) and enabling engines to request only needed columns.

Changes:

  • Introduces ColumnSelection and overloads of MetadataTable::Scan(...) / TimeTravelMetadataTable::Scan(...) to project columns via Schema::Select.
  • Refactors scanning to be implemented via ScanProjection(const Schema&) / ScanSnapshot(..., const Schema&) instead of overriding Scan().
  • Adds internal helpers to map projected schema columns to per-column append functions, and updates SnapshotsTable + tests accordingly.
File Description
src/​iceberg/​inspect/​metadata_table.h Adds ColumnSelection and new Scan overloads; refactors scan implementation to ScanProjection / projected-schema time travel.
src/​iceberg/​inspect/​metadata_table.cc Implements column selection via Schema::Select and routes scans through projected schemas.
src/​iceberg/​inspect/​metadata_table_columns_internal.h New internal helper to select per-column append functions based on projected schema.
src/​iceberg/​inspect/​snapshots_table.h Updates SnapshotsTable to implement ScanProjection(...) rather than Scan().
src/​iceberg/​inspect/​snapshots_table.cc Implements projected-column building for snapshots using selected append functions.
src/​iceberg/​inspect/​history_table.h /​ .cc Updates HistoryTable to ScanProjection(...) while keeping scan unsupported.
src/​iceberg/​test/​snapshots_table_test.cc Adds tests for column selection semantics on SnapshotsTable.
src/​iceberg/​test/​metadata_table_test.cc Adds tests for time-travel scans with column projection and HistoryTable behavior.

🧠 Review effort: Lite


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +67 to +77
ICEBERG_CHECK(field.has_value(), "Metadata table column '{}' is not in the schema",
projected_field.name());
if (*field->get().type() != *projected_field.type()) {
return NotSupported("Cannot select nested fields of metadata table column '{}'",
projected_field.name());
}
auto column = std::ranges::find(columns, projected_field.field_id(),
&MetadataTableColumn<Row>::field_id);
ICEBERG_CHECK(column != columns.end(),
"Metadata table column '{}' has no append function",
projected_field.name());
Comment thread src/iceberg/test/snapshots_table_test.cc Outdated
Comment thread src/iceberg/inspect/metadata_table.h Outdated
Scan(const ColumnSelection&) made Scan({}) on a TimeTravelMetadataTable
ambiguous with Scan(const SnapshotSelection&), since both are aggregates.
Use a distinct name so existing Scan({}) calls keep resolving to the
snapshot-selection overload.

Also assert on summary entries instead of the summary map's size, and
move the selection rules explained in test comments to the
ColumnSelection docs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 15, 2026 11:30

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@shangxinli shangxinli left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ScanColumns rename resolves the ambiguity I raised - Scan({}) still picks the SnapshotSelection overload. LGTM.

@manuzhang
manuzhang requested a review from wgtmac October 15, 2026 14:51
Comment thread src/iceberg/inspect/metadata_table.h Outdated
manuzhang and others added 3 commits October 28, 2026 22:30
…ilder

Replace ColumnSelection and the ScanColumns methods with a scan builder
shaped like TableScanBuilder, as requested in review. MetadataTable::NewScan()
returns a MetadataScanBuilder with Select(), CaseSensitive(), UseSnapshot(),
UseRef(), and AsOfTime(). Build() validates the options, projects the schema,
and returns a MetadataScan whose ToArrowArrayStream() reads the rows. Tables
without time travel reject snapshot selections when the scan is built.

Return InvalidArgument instead of ICEBERG_CHECK failures when a projected
column has no append function, and look up projected columns with
GetFieldById so nested field ids are rejected.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Make the scan surface a subset of the API proposed in apache#1066 so the two can
converge:

- MetadataTable::NewScan() is virtual and const, and ScanProjection() is const.
- MetadataScanBuilder::Select() resolves names immediately with the current
  CaseSensitive() setting and records errors for Build(); Project() sets the
  output schema directly, and Build() rejects a null projection.
- MetadataScan::ToArrowArrayStream() is virtual and const.
- Projection validation rejects unknown columns, type changes, and projecting a
  nullable column as required, with the same messages as apache#1066.
- Drop the time-travel options together with TimeTravelMetadataTable and
  SnapshotSelection, which apache#1066 also removes; nothing on main implements them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…Build()

Drop the MetadataScan class: separating a built scan from the Arrow stream it
reads belongs to the task-based design in apache#1066. Build() now validates the
options and returns the stream directly, and each call returns a new stream.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 28, 2026 15:12

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

Restore SnapshotSelection, TimeTravelMetadataTable, and supports_time_travel()
and pass the snapshot selection through the builder as UseSnapshot(), UseRef(),
and AsOfTime(), like TableScanBuilder. Build() returns NotSupported for tables
that do not support time travel, and conflicting options are validation errors.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 28, 2026 15:32

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants