blob: d0a214d07fa77e23df7ecbb1ed67e8e4f1f8b64f [file]
/*
* 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/table_scan.h"
#include <cstdint>
#include <utility>
#include "iceberg/expression/binder.h"
#include "iceberg/expression/expression.h"
#include "iceberg/expression/residual_evaluator.h"
#include "iceberg/manifest/manifest_entry.h"
#include "iceberg/manifest/manifest_group.h"
#include "iceberg/result.h"
#include "iceberg/schema.h"
#include "iceberg/snapshot.h"
#include "iceberg/table_metadata.h"
#include "iceberg/util/content_file_util.h"
#include "iceberg/util/macros.h"
#include "iceberg/util/snapshot_util_internal.h"
#include "iceberg/util/timepoint.h"
#include "iceberg/util/type_util.h"
namespace iceberg {
namespace {
const std::vector<std::string> kScanColumns = {
"snapshot_id", "file_path", "file_ordinal", "file_format",
"block_size_in_bytes", "file_size_in_bytes", "record_count", "partition",
"key_metadata", "split_offsets", "sort_order_id",
};
const std::vector<std::string> kStatsColumns = {
"value_counts", "null_value_counts", "nan_value_counts",
"lower_bounds", "upper_bounds", "column_sizes",
};
const std::vector<std::string> kScanColumnsWithStats = [] {
auto cols = kScanColumns;
cols.insert(cols.end(), kStatsColumns.begin(), kStatsColumns.end());
return cols;
}();
} // namespace
namespace internal {
Status TableScanContext::Validate() const {
ICEBERG_CHECK(columns_to_keep_stats.empty() || return_column_stats,
"Cannot select columns to keep stats when column stats are not returned");
ICEBERG_CHECK(projected_schema == nullptr || selected_columns.empty(),
"Cannot set projection schema and selected columns at the same time");
ICEBERG_CHECK(!snapshot_id.has_value() ||
(!from_snapshot_id.has_value() && !to_snapshot_id.has_value()),
"Cannot mix snapshot scan and incremental scan");
ICEBERG_CHECK(!min_rows_requested.has_value() || min_rows_requested.value() >= 0,
"Min rows requested cannot be negative");
return {};
}
bool IsScanCurrentLineage(const TableScanContext& context) {
return !context.from_snapshot_id.has_value() && !context.to_snapshot_id.has_value();
}
Result<int64_t> ToSnapshotIdInclusive(const TableScanContext& context,
const TableMetadata& metadata) {
// Get the branch's current snapshot ID if branch is set
std::shared_ptr<Snapshot> branch_snapshot;
const std::string& branch = context.branch;
if (!branch.empty()) {
auto iter = metadata.refs.find(branch);
ICEBERG_CHECK(iter != metadata.refs.end() && iter->second != nullptr,
"Cannot find branch: {}", branch);
ICEBERG_ASSIGN_OR_RAISE(branch_snapshot,
metadata.SnapshotById(iter->second->snapshot_id));
}
if (context.to_snapshot_id.has_value()) {
int64_t to_snapshot_id_value = context.to_snapshot_id.value();
if (branch_snapshot != nullptr) {
// Validate `to_snapshot_id` is on the current branch
ICEBERG_ASSIGN_OR_RAISE(
bool is_ancestor,
SnapshotUtil::IsAncestorOf(metadata, branch_snapshot->snapshot_id,
to_snapshot_id_value));
ICEBERG_CHECK(is_ancestor,
"End snapshot is not a valid snapshot on the current branch: {}",
branch);
}
return to_snapshot_id_value;
}
// If to_snapshot_id is not set, use branch's current snapshot if branch is set
if (branch_snapshot != nullptr) {
return branch_snapshot->snapshot_id;
}
// Get current snapshot from table's current snapshot
std::shared_ptr<Snapshot> current_snapshot;
ICEBERG_ASSIGN_OR_RAISE(current_snapshot, metadata.Snapshot());
ICEBERG_CHECK(current_snapshot != nullptr,
"End snapshot is not set and table has no current snapshot");
return current_snapshot->snapshot_id;
}
Result<std::optional<int64_t>> FromSnapshotIdExclusive(const TableScanContext& context,
const TableMetadata& metadata,
int64_t to_snapshot_id_inclusive) {
if (!context.from_snapshot_id.has_value()) {
return std::nullopt;
}
int64_t from_snapshot_id = context.from_snapshot_id.value();
// Validate `from_snapshot_id` is an ancestor of `to_snapshot_id_inclusive`
if (context.from_snapshot_id_inclusive) {
ICEBERG_ASSIGN_OR_RAISE(
bool is_ancestor,
SnapshotUtil::IsAncestorOf(metadata, to_snapshot_id_inclusive, from_snapshot_id));
ICEBERG_CHECK(
is_ancestor,
"Starting snapshot (inclusive) {} is not an ancestor of end snapshot {}",
from_snapshot_id, to_snapshot_id_inclusive);
// For inclusive behavior, return the parent snapshot ID (can be nullopt)
ICEBERG_ASSIGN_OR_RAISE(auto from_snapshot, metadata.SnapshotById(from_snapshot_id));
return from_snapshot->parent_snapshot_id;
}
// Validate there is an ancestor of `to_snapshot_id_inclusive` where parent is
// `from_snapshot_id`
ICEBERG_ASSIGN_OR_RAISE(bool is_parent_ancestor,
SnapshotUtil::IsParentAncestorOf(
metadata, to_snapshot_id_inclusive, from_snapshot_id));
ICEBERG_CHECK(
is_parent_ancestor,
"Starting snapshot (exclusive) {} is not a parent ancestor of end snapshot {}",
from_snapshot_id, to_snapshot_id_inclusive);
return from_snapshot_id;
}
} // namespace internal
ScanTask::~ScanTask() = default;
// FileScanTask implementation
FileScanTask::FileScanTask(std::shared_ptr<DataFile> data_file,
std::vector<std::shared_ptr<DataFile>> delete_files,
std::shared_ptr<Expression> residual_filter)
: data_file_(std::move(data_file)),
delete_files_(std::move(delete_files)),
residual_filter_(std::move(residual_filter)) {
ICEBERG_DCHECK(data_file_ != nullptr, "Data file cannot be null for FileScanTask");
}
int64_t FileScanTask::size_bytes() const { return data_file_->file_size_in_bytes; }
int32_t FileScanTask::files_count() const { return 1; }
int64_t FileScanTask::estimated_row_count() const { return data_file_->record_count; }
// ChangelogScanTask implementation
int64_t ChangelogScanTask::size_bytes() const {
int64_t total_size = data_file_->file_size_in_bytes;
for (const auto& delete_file : delete_files_) {
ICEBERG_DCHECK(delete_file->content_size_in_bytes.has_value(),
"Delete file content size must be available");
total_size +=
(delete_file->IsDeletionVector() ? delete_file->content_size_in_bytes.value()
: delete_file->file_size_in_bytes);
}
return total_size;
}
int32_t ChangelogScanTask::files_count() const { return 1 + delete_files_.size(); }
int64_t ChangelogScanTask::estimated_row_count() const {
return data_file_->record_count;
}
// Generic template implementation for Make
template <typename ScanType>
Result<std::unique_ptr<TableScanBuilder<ScanType>>> TableScanBuilder<ScanType>::Make(
std::shared_ptr<TableMetadata> metadata, std::shared_ptr<FileIO> io) {
ICEBERG_PRECHECK(metadata != nullptr, "Table metadata cannot be null");
ICEBERG_PRECHECK(io != nullptr, "FileIO cannot be null");
return std::unique_ptr<TableScanBuilder<ScanType>>(
new TableScanBuilder<ScanType>(std::move(metadata), std::move(io)));
}
template <typename ScanType>
TableScanBuilder<ScanType>::TableScanBuilder(
std::shared_ptr<TableMetadata> table_metadata, std::shared_ptr<FileIO> file_io)
: metadata_(std::move(table_metadata)), io_(std::move(file_io)) {}
template <typename ScanType>
TableScanBuilder<ScanType>& TableScanBuilder<ScanType>::Option(std::string key,
std::string value) {
context_.options[std::move(key)] = std::move(value);
return *this;
}
template <typename ScanType>
TableScanBuilder<ScanType>& TableScanBuilder<ScanType>::Project(
std::shared_ptr<Schema> schema) {
context_.projected_schema = std::move(schema);
return *this;
}
template <typename ScanType>
TableScanBuilder<ScanType>& TableScanBuilder<ScanType>::CaseSensitive(
bool case_sensitive) {
context_.case_sensitive = case_sensitive;
return *this;
}
template <typename ScanType>
TableScanBuilder<ScanType>& TableScanBuilder<ScanType>::IncludeColumnStats() {
context_.return_column_stats = true;
context_.columns_to_keep_stats.clear();
requested_column_stats_.reset();
return *this;
}
template <typename ScanType>
TableScanBuilder<ScanType>& TableScanBuilder<ScanType>::IncludeColumnStats(
const std::vector<std::string>& requested_columns) {
context_.return_column_stats = true;
requested_column_stats_ = requested_columns;
return *this;
}
template <typename ScanType>
TableScanBuilder<ScanType>& TableScanBuilder<ScanType>::Select(
const std::vector<std::string>& column_names) {
context_.selected_columns = column_names;
return *this;
}
template <typename ScanType>
TableScanBuilder<ScanType>& TableScanBuilder<ScanType>::Filter(
std::shared_ptr<Expression> filter) {
context_.filter = std::move(filter);
return *this;
}
template <typename ScanType>
TableScanBuilder<ScanType>& TableScanBuilder<ScanType>::IgnoreResiduals() {
context_.ignore_residuals = true;
return *this;
}
template <typename ScanType>
TableScanBuilder<ScanType>& TableScanBuilder<ScanType>::MinRowsRequested(
int64_t num_rows) {
context_.min_rows_requested = num_rows;
return *this;
}
template <typename ScanType>
TableScanBuilder<ScanType>& TableScanBuilder<ScanType>::PlanWith(Executor& executor) {
context_.plan_executor = std::ref(executor);
return *this;
}
template <typename ScanType>
TableScanBuilder<ScanType>& TableScanBuilder<ScanType>::UseSnapshot(int64_t snapshot_id) {
ICEBERG_BUILDER_CHECK(!context_.snapshot_id.has_value(),
"Cannot override snapshot, already set snapshot id={}",
context_.snapshot_id.value());
ICEBERG_BUILDER_ASSIGN_OR_RETURN(std::ignore, metadata_->SnapshotById(snapshot_id));
context_.snapshot_id = snapshot_id;
return *this;
}
template <typename ScanType>
TableScanBuilder<ScanType>& TableScanBuilder<ScanType>::UseRef(const std::string& ref) {
if (ref == SnapshotRef::kMainBranch) {
context_.snapshot_id.reset();
return *this;
}
ICEBERG_BUILDER_CHECK(!context_.snapshot_id.has_value(),
"Cannot override ref, already set snapshot id={}",
context_.snapshot_id.value());
auto iter = metadata_->refs.find(ref);
ICEBERG_BUILDER_CHECK(iter != metadata_->refs.end(), "Cannot find ref {}", ref);
ICEBERG_BUILDER_CHECK(iter->second != nullptr, "Ref {} is null", ref);
const int64_t snapshot_id = iter->second->snapshot_id;
ICEBERG_BUILDER_ASSIGN_OR_RETURN(std::ignore, metadata_->SnapshotById(snapshot_id));
context_.snapshot_id = snapshot_id;
return *this;
}
template <typename ScanType>
TableScanBuilder<ScanType>& TableScanBuilder<ScanType>::AsOfTime(
int64_t timestamp_millis) {
auto time_point_ms = TimePointMsFromUnixMs(timestamp_millis);
ICEBERG_BUILDER_ASSIGN_OR_RETURN(
auto snapshot_id, SnapshotUtil::SnapshotIdAsOfTime(*metadata_, time_point_ms));
return UseSnapshot(snapshot_id);
}
template <typename ScanType>
TableScanBuilder<ScanType>& TableScanBuilder<ScanType>::FromSnapshot(
int64_t from_snapshot_id, bool inclusive)
requires IsIncrementalScan<ScanType>
{
if (inclusive) {
ICEBERG_BUILDER_ASSIGN_OR_RETURN(std::ignore,
metadata_->SnapshotById(from_snapshot_id));
}
this->context_.from_snapshot_id = from_snapshot_id;
this->context_.from_snapshot_id_inclusive = inclusive;
return *this;
}
template <typename ScanType>
TableScanBuilder<ScanType>& TableScanBuilder<ScanType>::FromSnapshot(
const std::string& ref, bool inclusive)
requires IsIncrementalScan<ScanType>
{
auto iter = metadata_->refs.find(ref);
ICEBERG_BUILDER_CHECK(iter != metadata_->refs.end(), "Cannot find ref: {}", ref);
ICEBERG_BUILDER_CHECK(iter->second != nullptr, "Ref {} is null", ref);
ICEBERG_BUILDER_CHECK(iter->second->type() == SnapshotRefType::kTag,
"Ref {} is not a tag", ref);
return FromSnapshot(iter->second->snapshot_id, inclusive);
}
template <typename ScanType>
TableScanBuilder<ScanType>& TableScanBuilder<ScanType>::ToSnapshot(int64_t to_snapshot_id)
requires IsIncrementalScan<ScanType>
{
ICEBERG_BUILDER_ASSIGN_OR_RETURN(std::ignore, metadata_->SnapshotById(to_snapshot_id));
context_.to_snapshot_id = to_snapshot_id;
return *this;
}
template <typename ScanType>
TableScanBuilder<ScanType>& TableScanBuilder<ScanType>::ToSnapshot(const std::string& ref)
requires IsIncrementalScan<ScanType>
{
auto iter = metadata_->refs.find(ref);
ICEBERG_BUILDER_CHECK(iter != metadata_->refs.end(), "Cannot find ref: {}", ref);
ICEBERG_BUILDER_CHECK(iter->second != nullptr, "Ref {} is null", ref);
ICEBERG_BUILDER_CHECK(iter->second->type() == SnapshotRefType::kTag,
"Ref {} is not a tag", ref);
return ToSnapshot(iter->second->snapshot_id);
}
template <typename ScanType>
TableScanBuilder<ScanType>& TableScanBuilder<ScanType>::UseBranch(
const std::string& branch)
requires IsIncrementalScan<ScanType>
{
auto iter = metadata_->refs.find(branch);
ICEBERG_BUILDER_CHECK(iter != metadata_->refs.end(), "Cannot find ref: {}", branch);
ICEBERG_BUILDER_CHECK(iter->second != nullptr, "Ref {} is null", branch);
ICEBERG_BUILDER_CHECK(iter->second->type() == SnapshotRefType::kBranch,
"Ref {} is not a branch", branch);
context_.branch = branch;
return *this;
}
template <typename ScanType>
Status TableScanBuilder<ScanType>::ResolveColumnStatsSelection() {
if (!requested_column_stats_.has_value()) {
return {};
}
context_.columns_to_keep_stats.clear();
context_.columns_to_keep_stats.reserve(requested_column_stats_->size());
ICEBERG_ASSIGN_OR_RAISE(auto schema_ref, ResolveSnapshotSchema());
const auto& schema = schema_ref.get();
for (const auto& column_name : *requested_column_stats_) {
ICEBERG_ASSIGN_OR_RAISE(auto field, schema->FindFieldByName(column_name));
ICEBERG_CHECK(field.has_value(), "Cannot find stats column: {}", column_name);
context_.columns_to_keep_stats.insert(field.value().get().field_id());
}
return {};
}
template <typename ScanType>
Result<std::reference_wrapper<const std::shared_ptr<Schema>>>
TableScanBuilder<ScanType>::ResolveSnapshotSchema() {
if (snapshot_schema_ == nullptr) {
if (context_.snapshot_id.has_value()) {
ICEBERG_ASSIGN_OR_RAISE(auto snapshot,
metadata_->SnapshotById(*context_.snapshot_id));
int32_t schema_id = snapshot->schema_id.value_or(Schema::kInitialSchemaId);
ICEBERG_ASSIGN_OR_RAISE(snapshot_schema_, metadata_->SchemaById(schema_id));
} else {
ICEBERG_ASSIGN_OR_RAISE(snapshot_schema_, metadata_->Schema());
}
}
ICEBERG_CHECK(snapshot_schema_ != nullptr, "Snapshot schema is null");
return snapshot_schema_;
}
template <typename ScanType>
Result<std::unique_ptr<ScanType>> TableScanBuilder<ScanType>::Build() {
ICEBERG_RETURN_UNEXPECTED(CheckErrors());
ICEBERG_RETURN_UNEXPECTED(ResolveColumnStatsSelection());
ICEBERG_RETURN_UNEXPECTED(context_.Validate());
ICEBERG_ASSIGN_OR_RAISE(auto schema, ResolveSnapshotSchema());
return ScanType::Make(metadata_, schema.get(), io_, std::move(context_));
}
// Explicit template instantiations
template class ICEBERG_TEMPLATE_EXPORT TableScanBuilder<DataTableScan>;
template class ICEBERG_TEMPLATE_EXPORT TableScanBuilder<IncrementalAppendScan>;
template class ICEBERG_TEMPLATE_EXPORT TableScanBuilder<IncrementalChangelogScan>;
TableScan::TableScan(std::shared_ptr<TableMetadata> metadata,
std::shared_ptr<Schema> schema, std::shared_ptr<FileIO> file_io,
internal::TableScanContext context)
: metadata_(std::move(metadata)),
schema_(std::move(schema)),
io_(std::move(file_io)),
context_(std::move(context)) {}
TableScan::~TableScan() = default;
const std::shared_ptr<TableMetadata>& TableScan::metadata() const { return metadata_; }
Result<std::shared_ptr<Snapshot>> TableScan::snapshot() const {
auto snapshot_id = context_.snapshot_id ? context_.snapshot_id.value()
: metadata_->current_snapshot_id;
if (snapshot_id == kInvalidSnapshotId) {
return std::shared_ptr<Snapshot>{nullptr};
}
return metadata_->SnapshotById(snapshot_id);
}
Result<std::shared_ptr<Schema>> TableScan::schema() const {
return ResolveProjectedSchema();
}
const internal::TableScanContext& TableScan::context() const { return context_; }
const std::shared_ptr<FileIO>& TableScan::io() const { return io_; }
const std::shared_ptr<Expression>& TableScan::filter() const {
const static std::shared_ptr<Expression> true_expr = True::Instance();
if (!context_.filter) {
return true_expr;
}
return context_.filter;
}
bool TableScan::is_case_sensitive() const { return context_.case_sensitive; }
Result<std::reference_wrapper<const std::shared_ptr<Schema>>>
TableScan::ResolveProjectedSchema() const {
if (projected_schema_ != nullptr) {
return projected_schema_;
}
if (!context_.selected_columns.empty()) {
std::unordered_set<int32_t> required_field_ids;
// Include columns referenced by filter
if (context_.filter != nullptr) {
ICEBERG_ASSIGN_OR_RAISE(auto is_bound, IsBoundVisitor::IsBound(context_.filter));
if (is_bound) {
ICEBERG_ASSIGN_OR_RAISE(required_field_ids,
ReferenceVisitor::GetReferencedFieldIds(context_.filter));
} else {
ICEBERG_ASSIGN_OR_RAISE(auto filter, Binder::Bind(*schema_, context_.filter,
context_.case_sensitive));
ICEBERG_ASSIGN_OR_RAISE(required_field_ids,
ReferenceVisitor::GetReferencedFieldIds(filter));
}
}
// Include columns selected by option
ICEBERG_ASSIGN_OR_RAISE(auto selected, schema_->Select(context_.selected_columns,
context_.case_sensitive));
ICEBERG_ASSIGN_OR_RAISE(
auto selected_field_ids,
GetProjectedIdsVisitor::GetProjectedIds(*selected, /*include_struct_ids=*/true));
required_field_ids.insert(std::make_move_iterator(selected_field_ids.begin()),
std::make_move_iterator(selected_field_ids.end()));
ICEBERG_ASSIGN_OR_RAISE(projected_schema_, schema_->Project(required_field_ids));
} else if (context_.projected_schema != nullptr) {
projected_schema_ = context_.projected_schema;
} else {
projected_schema_ = schema_;
}
return projected_schema_;
}
const std::vector<std::string>& TableScan::ScanColumns() const {
return context_.return_column_stats ? kScanColumnsWithStats : kScanColumns;
}
Result<std::unique_ptr<DataTableScan>> DataTableScan::Make(
std::shared_ptr<TableMetadata> metadata, std::shared_ptr<Schema> schema,
std::shared_ptr<FileIO> io, internal::TableScanContext context) {
ICEBERG_PRECHECK(metadata != nullptr, "Table metadata cannot be null");
ICEBERG_PRECHECK(schema != nullptr, "Schema cannot be null");
ICEBERG_PRECHECK(io != nullptr, "FileIO cannot be null");
return std::unique_ptr<DataTableScan>(new DataTableScan(
std::move(metadata), std::move(schema), std::move(io), std::move(context)));
}
Result<std::vector<std::shared_ptr<FileScanTask>>> DataTableScan::PlanFiles() const {
ICEBERG_ASSIGN_OR_RAISE(auto snapshot, this->snapshot());
if (!snapshot) {
return std::vector<std::shared_ptr<FileScanTask>>{};
}
TableMetadataCache metadata_cache(metadata_.get());
ICEBERG_ASSIGN_OR_RAISE(auto specs_by_id, metadata_cache.GetPartitionSpecsById());
SnapshotCache snapshot_cache(snapshot.get());
ICEBERG_ASSIGN_OR_RAISE(auto data_manifests, snapshot_cache.DataManifests(io_));
ICEBERG_ASSIGN_OR_RAISE(auto delete_manifests, snapshot_cache.DeleteManifests(io_));
ICEBERG_ASSIGN_OR_RAISE(
auto manifest_group,
ManifestGroup::Make(io_, schema_, specs_by_id,
{data_manifests.begin(), data_manifests.end()},
{delete_manifests.begin(), delete_manifests.end()}));
manifest_group->CaseSensitive(context_.case_sensitive)
.Select(ScanColumns())
.FilterData(filter())
.IgnoreDeleted()
.ColumnsToKeepStats(context_.columns_to_keep_stats)
.PlanWith(context_.plan_executor);
if (context_.ignore_residuals) {
manifest_group->IgnoreResiduals();
}
return manifest_group->PlanFiles();
}
// Friend function template for IncrementalScan that implements the shared PlanFiles
// logic. It resolves the from/to snapshot range from the scan context and delegates
// to the two-arg virtual PlanFiles() override in the concrete subclass.
// Defined as a friend to access the protected two-arg PlanFiles().
template <typename ScanTaskType>
Result<std::vector<std::shared_ptr<ScanTaskType>>> ResolvePlanFiles(
const IncrementalScan<ScanTaskType>& scan) {
if (IsScanCurrentLineage(scan.context())) {
if (scan.metadata()->current_snapshot_id == kInvalidSnapshotId) {
return std::vector<std::shared_ptr<ScanTaskType>>{};
}
}
ICEBERG_ASSIGN_OR_RAISE(
int64_t to_snapshot_id_inclusive,
internal::ToSnapshotIdInclusive(scan.context(), *scan.metadata()));
ICEBERG_ASSIGN_OR_RAISE(
std::optional<int64_t> from_snapshot_id_exclusive,
internal::FromSnapshotIdExclusive(scan.context(), *scan.metadata(),
to_snapshot_id_inclusive));
return scan.PlanFiles(from_snapshot_id_exclusive, to_snapshot_id_inclusive);
}
// IncrementalAppendScan implementation
Result<std::unique_ptr<IncrementalAppendScan>> IncrementalAppendScan::Make(
std::shared_ptr<TableMetadata> metadata, std::shared_ptr<Schema> schema,
std::shared_ptr<FileIO> io, internal::TableScanContext context) {
ICEBERG_PRECHECK(metadata != nullptr, "Table metadata cannot be null");
ICEBERG_PRECHECK(schema != nullptr, "Schema cannot be null");
ICEBERG_PRECHECK(io != nullptr, "FileIO cannot be null");
return std::unique_ptr<IncrementalAppendScan>(new IncrementalAppendScan(
std::move(metadata), std::move(schema), std::move(io), std::move(context)));
}
Result<std::vector<std::shared_ptr<FileScanTask>>> IncrementalAppendScan::PlanFiles()
const {
return ResolvePlanFiles<FileScanTask>(*this);
}
Result<std::vector<std::shared_ptr<FileScanTask>>> IncrementalAppendScan::PlanFiles(
std::optional<int64_t> from_snapshot_id_exclusive,
int64_t to_snapshot_id_inclusive) const {
ICEBERG_ASSIGN_OR_RAISE(
auto ancestors_snapshots,
SnapshotUtil::AncestorsBetween(*metadata_, to_snapshot_id_inclusive,
from_snapshot_id_exclusive));
std::vector<std::shared_ptr<Snapshot>> append_snapshots;
std::ranges::copy_if(ancestors_snapshots, std::back_inserter(append_snapshots),
[](const auto& snapshot) {
return snapshot != nullptr &&
snapshot->Operation().has_value() &&
snapshot->Operation().value() == DataOperation::kAppend;
});
if (append_snapshots.empty()) {
return std::vector<std::shared_ptr<FileScanTask>>{};
}
std::unordered_set<int64_t> snapshot_ids;
std::ranges::transform(append_snapshots,
std::inserter(snapshot_ids, snapshot_ids.end()),
[](const auto& snapshot) { return snapshot->snapshot_id; });
std::unordered_set<ManifestFile> data_manifests;
for (const auto& snapshot : append_snapshots) {
SnapshotCache snapshot_cache(snapshot.get());
ICEBERG_ASSIGN_OR_RAISE(auto manifests, snapshot_cache.DataManifests(io_));
std::ranges::copy_if(manifests, std::inserter(data_manifests, data_manifests.end()),
[&snapshot_ids](const ManifestFile& manifest) {
return snapshot_ids.contains(manifest.added_snapshot_id);
});
}
if (data_manifests.empty()) {
return std::vector<std::shared_ptr<FileScanTask>>{};
}
TableMetadataCache metadata_cache(metadata_.get());
ICEBERG_ASSIGN_OR_RAISE(auto specs_by_id, metadata_cache.GetPartitionSpecsById());
ICEBERG_ASSIGN_OR_RAISE(
auto manifest_group,
ManifestGroup::Make(
io_, schema_, specs_by_id,
std::vector<ManifestFile>(data_manifests.begin(), data_manifests.end()), {}));
manifest_group->CaseSensitive(context_.case_sensitive)
.Select(ScanColumns())
.FilterData(filter())
.FilterManifestEntries([&snapshot_ids](const ManifestEntry& entry) {
return entry.snapshot_id.has_value() &&
snapshot_ids.contains(entry.snapshot_id.value()) &&
entry.status == ManifestStatus::kAdded;
})
.IgnoreDeleted()
.ColumnsToKeepStats(context_.columns_to_keep_stats)
.PlanWith(context_.plan_executor);
if (context_.ignore_residuals) {
manifest_group->IgnoreResiduals();
}
return manifest_group->PlanFiles();
}
// IncrementalChangelogScan implementation
Result<std::unique_ptr<IncrementalChangelogScan>> IncrementalChangelogScan::Make(
std::shared_ptr<TableMetadata> metadata, std::shared_ptr<Schema> schema,
std::shared_ptr<FileIO> io, internal::TableScanContext context) {
ICEBERG_PRECHECK(metadata != nullptr, "Table metadata cannot be null");
ICEBERG_PRECHECK(schema != nullptr, "Schema cannot be null");
ICEBERG_PRECHECK(io != nullptr, "FileIO cannot be null");
return std::unique_ptr<IncrementalChangelogScan>(new IncrementalChangelogScan(
std::move(metadata), std::move(schema), std::move(io), std::move(context)));
}
Result<std::vector<std::shared_ptr<ChangelogScanTask>>>
IncrementalChangelogScan::PlanFiles() const {
return ResolvePlanFiles<ChangelogScanTask>(*this);
}
Result<std::vector<std::shared_ptr<ChangelogScanTask>>>
IncrementalChangelogScan::PlanFiles(std::optional<int64_t> from_snapshot_id_exclusive,
int64_t to_snapshot_id_inclusive) const {
ICEBERG_ASSIGN_OR_RAISE(
auto ancestors_snapshots,
SnapshotUtil::AncestorsBetween(*metadata_, to_snapshot_id_inclusive,
from_snapshot_id_exclusive));
std::vector<std::pair<std::shared_ptr<Snapshot>, std::unique_ptr<SnapshotCache>>>
changelog_snapshots;
for (const auto& snapshot : std::ranges::reverse_view(ancestors_snapshots)) {
auto operation = snapshot->Operation();
if (!operation.has_value() || operation.value() != DataOperation::kReplace) {
auto snapshot_cache = std::make_unique<SnapshotCache>(snapshot.get());
ICEBERG_ASSIGN_OR_RAISE(auto delete_manifests,
snapshot_cache->DeleteManifests(io_));
if (!delete_manifests.empty()) {
return NotSupported(
"Delete files are currently not supported in changelog scans");
}
changelog_snapshots.emplace_back(snapshot, std::move(snapshot_cache));
}
}
if (changelog_snapshots.empty()) {
return std::vector<std::shared_ptr<ChangelogScanTask>>{};
}
std::unordered_set<int64_t> snapshot_ids;
std::unordered_map<int64_t, int32_t> snapshot_ordinals;
for (const auto& snapshot : changelog_snapshots) {
ICEBERG_PRECHECK(
std::cmp_less_equal(snapshot_ids.size(), std::numeric_limits<int32_t>::max()),
"Number of snapshots in changelog scan exceeds maximum supported");
snapshot_ids.insert(snapshot.first->snapshot_id);
snapshot_ordinals.try_emplace(snapshot.first->snapshot_id,
static_cast<int32_t>(snapshot_ordinals.size()));
}
std::vector<ManifestFile> data_manifests;
std::unordered_set<std::string> seen_manifest_paths;
for (const auto& snapshot : changelog_snapshots) {
ICEBERG_ASSIGN_OR_RAISE(auto manifests, snapshot.second->DataManifests(io_));
for (auto& manifest : manifests) {
if (snapshot_ids.contains(manifest.added_snapshot_id) &&
seen_manifest_paths.insert(manifest.manifest_path).second) {
data_manifests.push_back(manifest);
}
}
}
if (data_manifests.empty()) {
return std::vector<std::shared_ptr<ChangelogScanTask>>{};
}
TableMetadataCache metadata_cache(metadata_.get());
ICEBERG_ASSIGN_OR_RAISE(auto specs_by_id, metadata_cache.GetPartitionSpecsById());
ICEBERG_ASSIGN_OR_RAISE(
auto manifest_group,
ManifestGroup::Make(io_, schema_, specs_by_id, std::move(data_manifests),
/*delete_manifests=*/{}));
manifest_group->CaseSensitive(context_.case_sensitive)
.Select(ScanColumns())
.FilterData(filter())
.FilterManifestEntries([&snapshot_ids](const ManifestEntry& entry) {
return entry.snapshot_id.has_value() &&
snapshot_ids.contains(entry.snapshot_id.value());
})
.IgnoreExisting()
.ColumnsToKeepStats(context_.columns_to_keep_stats)
.PlanWith(context_.plan_executor);
if (context_.ignore_residuals) {
manifest_group->IgnoreResiduals();
}
auto create_tasks_func =
[&snapshot_ordinals](
std::vector<ManifestEntry>&& entries,
const TaskContext& ctx) -> Result<std::vector<std::shared_ptr<ScanTask>>> {
std::vector<std::shared_ptr<ScanTask>> tasks;
tasks.reserve(entries.size());
for (auto& entry : entries) {
ICEBERG_PRECHECK(entry.snapshot_id.has_value() && entry.data_file,
"Invalid manifest entry with missing snapshot id or data file");
int64_t commit_snapshot_id = entry.snapshot_id.value();
auto ordinal_it = snapshot_ordinals.find(commit_snapshot_id);
ICEBERG_PRECHECK(ordinal_it != snapshot_ordinals.end(),
"Invalid manifest entry with missing snapshot ordinal");
int32_t change_ordinal = ordinal_it->second;
if (ctx.drop_stats) {
ContentFileUtil::DropAllStats(*entry.data_file);
} else if (!ctx.columns_to_keep_stats.empty()) {
ContentFileUtil::DropUnselectedStats(*entry.data_file, ctx.columns_to_keep_stats);
}
ICEBERG_ASSIGN_OR_RAISE(auto residual,
ctx.residuals->ResidualFor(entry.data_file->partition));
switch (entry.status) {
case ManifestStatus::kAdded:
tasks.push_back(std::make_shared<AddedRowsScanTask>(
change_ordinal, commit_snapshot_id, std::move(entry.data_file),
std::vector<std::shared_ptr<DataFile>>{}, std::move(residual)));
break;
case ManifestStatus::kDeleted:
tasks.push_back(std::make_shared<DeletedDataFileScanTask>(
change_ordinal, commit_snapshot_id, std::move(entry.data_file),
std::vector<std::shared_ptr<DataFile>>{}, std::move(residual)));
break;
case ManifestStatus::kExisting:
return InvalidArgument("Unexpected entry status: EXISTING");
}
}
return tasks;
};
ICEBERG_ASSIGN_OR_RAISE(auto tasks, manifest_group->Plan(create_tasks_func));
return tasks | std::views::transform([](const auto& task) {
return std::static_pointer_cast<ChangelogScanTask>(task);
}) |
std::ranges::to<std::vector>();
}
} // namespace iceberg