Repository navigation
feat(inspect): add task-based scans for snapshots and history #1066
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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,81 @@ 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 && ancestors.insert(current).second;) { | ||
| auto it = snapshots_by_id.find(current); | ||
| if (it == snapshots_by_id.end() || !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 { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The follow methods look same as SnapshotTable's. Can they be moved to superclass?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Both scans now use the base |
||
| return MetadataScanTaskStream::FromVector({task_}); | ||
| } | ||
|
|
||
| } // namespace iceberg | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: the cycle guard here isn't tested against an actual cycle in the snapshot graph, so it's defensive-only right now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added
CyclicAncestryTerminateswith a two-snapshot cycle. It checks the output and makes sure the scan finishes.