1919
2020#include " iceberg/table_scan.h"
2121
22+ #include < cstdint>
2223#include < cstring>
2324#include < iterator>
25+ #include < utility>
2426
2527#include " iceberg/expression/binder.h"
2628#include " iceberg/expression/expression.h"
29+ #include " iceberg/expression/residual_evaluator.h"
2730#include " iceberg/file_reader.h"
2831#include " iceberg/manifest/manifest_entry.h"
2932#include " iceberg/manifest/manifest_group.h"
3033#include " iceberg/result.h"
3134#include " iceberg/schema.h"
3235#include " iceberg/snapshot.h"
3336#include " iceberg/table_metadata.h"
37+ #include " iceberg/util/content_file_util.h"
3438#include " iceberg/util/macros.h"
3539#include " iceberg/util/snapshot_util_internal.h"
3640#include " iceberg/util/timepoint.h"
@@ -294,6 +298,26 @@ Result<ArrowArrayStream> FileScanTask::ToArrow(
294298 return MakeArrowArrayStream (std::move (reader));
295299}
296300
301+ // ChangelogScanTask implementation
302+
303+ int64_t ChangelogScanTask::size_bytes () const {
304+ int64_t total_size = data_file_->file_size_in_bytes ;
305+ for (const auto & delete_file : delete_files_) {
306+ ICEBERG_DCHECK (delete_file->content_size_in_bytes .has_value (),
307+ " Delete file content size must be available" );
308+ total_size +=
309+ (delete_file->IsDeletionVector () ? delete_file->content_size_in_bytes .value ()
310+ : delete_file->file_size_in_bytes );
311+ }
312+ return total_size;
313+ }
314+
315+ int32_t ChangelogScanTask::files_count () const { return 1 + delete_files_.size (); }
316+
317+ int64_t ChangelogScanTask::estimated_row_count () const {
318+ return data_file_->record_count ;
319+ }
320+
297321// Generic template implementation for Make
298322template <typename ScanType>
299323Result<std::unique_ptr<TableScanBuilder<ScanType>>> TableScanBuilder<ScanType>::Make(
@@ -747,11 +771,13 @@ Result<std::vector<std::shared_ptr<FileScanTask>>> IncrementalAppendScan::PlanFi
747771// IncrementalChangelogScan implementation
748772
749773Result<std::unique_ptr<IncrementalChangelogScan>> IncrementalChangelogScan::Make (
750- [[maybe_unused]] std::shared_ptr<TableMetadata> metadata,
751- [[maybe_unused]] std::shared_ptr<Schema> schema,
752- [[maybe_unused]] std::shared_ptr<FileIO> io,
753- [[maybe_unused]] internal::TableScanContext context) {
754- return NotImplemented (" IncrementalChangelogScan is not implemented" );
774+ std::shared_ptr<TableMetadata> metadata, std::shared_ptr<Schema> schema,
775+ std::shared_ptr<FileIO> io, internal::TableScanContext context) {
776+ ICEBERG_PRECHECK (metadata != nullptr , " Table metadata cannot be null" );
777+ ICEBERG_PRECHECK (schema != nullptr , " Schema cannot be null" );
778+ ICEBERG_PRECHECK (io != nullptr , " FileIO cannot be null" );
779+ return std::unique_ptr<IncrementalChangelogScan>(new IncrementalChangelogScan (
780+ std::move (metadata), std::move (schema), std::move (io), std::move (context)));
755781}
756782
757783Result<std::vector<std::shared_ptr<ChangelogScanTask>>>
@@ -762,7 +788,129 @@ IncrementalChangelogScan::PlanFiles() const {
762788Result<std::vector<std::shared_ptr<ChangelogScanTask>>>
763789IncrementalChangelogScan::PlanFiles (std::optional<int64_t > from_snapshot_id_exclusive,
764790 int64_t to_snapshot_id_inclusive) const {
765- return NotImplemented (" IncrementalChangelogScan::PlanFiles is not implemented" );
791+ ICEBERG_ASSIGN_OR_RAISE (
792+ auto ancestors_snapshots,
793+ SnapshotUtil::AncestorsBetween (*metadata_, to_snapshot_id_inclusive,
794+ from_snapshot_id_exclusive));
795+
796+ std::vector<std::pair<std::shared_ptr<Snapshot>, std::unique_ptr<SnapshotCache>>>
797+ changelog_snapshots;
798+
799+ for (const auto & snapshot : std::ranges::reverse_view (ancestors_snapshots)) {
800+ auto operation = snapshot->Operation ();
801+ if (!operation.has_value () || operation.value () != DataOperation::kReplace ) {
802+ auto snapshot_cache = std::make_unique<SnapshotCache>(snapshot.get ());
803+ ICEBERG_ASSIGN_OR_RAISE (auto delete_manifests,
804+ snapshot_cache->DeleteManifests (io_));
805+ if (!delete_manifests.empty ()) {
806+ return NotSupported (
807+ " Delete files are currently not supported in changelog scans" );
808+ }
809+ changelog_snapshots.emplace_back (snapshot, std::move (snapshot_cache));
810+ }
811+ }
812+ if (changelog_snapshots.empty ()) {
813+ return std::vector<std::shared_ptr<ChangelogScanTask>>{};
814+ }
815+
816+ std::unordered_set<int64_t > snapshot_ids;
817+ std::unordered_map<int64_t , int32_t > snapshot_ordinals;
818+ for (const auto & snapshot : changelog_snapshots) {
819+ ICEBERG_PRECHECK (
820+ std::cmp_less_equal (snapshot_ids.size (), std::numeric_limits<int32_t >::max ()),
821+ " Number of snapshots in changelog scan exceeds maximum supported" );
822+ snapshot_ids.insert (snapshot.first ->snapshot_id );
823+ snapshot_ordinals.try_emplace (snapshot.first ->snapshot_id ,
824+ static_cast <int32_t >(snapshot_ordinals.size ()));
825+ }
826+
827+ std::vector<ManifestFile> data_manifests;
828+ std::unordered_set<std::string> seen_manifest_paths;
829+ for (const auto & snapshot : changelog_snapshots) {
830+ ICEBERG_ASSIGN_OR_RAISE (auto manifests, snapshot.second ->DataManifests (io_));
831+ for (auto & manifest : manifests) {
832+ if (snapshot_ids.contains (manifest.added_snapshot_id ) &&
833+ seen_manifest_paths.insert (manifest.manifest_path ).second ) {
834+ data_manifests.push_back (manifest);
835+ }
836+ }
837+ }
838+ if (data_manifests.empty ()) {
839+ return std::vector<std::shared_ptr<ChangelogScanTask>>{};
840+ }
841+
842+ TableMetadataCache metadata_cache (metadata_.get ());
843+ ICEBERG_ASSIGN_OR_RAISE (auto specs_by_id, metadata_cache.GetPartitionSpecsById ());
844+
845+ ICEBERG_ASSIGN_OR_RAISE (
846+ auto manifest_group,
847+ ManifestGroup::Make (io_, schema_, specs_by_id, std::move (data_manifests),
848+ /* delete_manifests=*/ {}));
849+
850+ manifest_group->CaseSensitive (context_.case_sensitive )
851+ .Select (ScanColumns ())
852+ .FilterData (filter ())
853+ .FilterManifestEntries ([&snapshot_ids](const ManifestEntry& entry) {
854+ return entry.snapshot_id .has_value () &&
855+ snapshot_ids.contains (entry.snapshot_id .value ());
856+ })
857+ .IgnoreExisting ()
858+ .ColumnsToKeepStats (context_.columns_to_keep_stats );
859+
860+ if (context_.ignore_residuals ) {
861+ manifest_group->IgnoreResiduals ();
862+ }
863+
864+ auto create_tasks_func =
865+ [&snapshot_ordinals](
866+ std::vector<ManifestEntry>&& entries,
867+ const TaskContext& ctx) -> Result<std::vector<std::shared_ptr<ScanTask>>> {
868+ std::vector<std::shared_ptr<ScanTask>> tasks;
869+ tasks.reserve (entries.size ());
870+
871+ for (auto & entry : entries) {
872+ ICEBERG_PRECHECK (entry.snapshot_id .has_value () && entry.data_file ,
873+ " Invalid manifest entry with missing snapshot id or data file" );
874+
875+ int64_t commit_snapshot_id = entry.snapshot_id .value ();
876+ auto ordinal_it = snapshot_ordinals.find (commit_snapshot_id);
877+ ICEBERG_PRECHECK (ordinal_it != snapshot_ordinals.end (),
878+ " Invalid manifest entry with missing snapshot ordinal" );
879+
880+ int32_t change_ordinal = ordinal_it->second ;
881+
882+ if (ctx.drop_stats ) {
883+ ContentFileUtil::DropAllStats (*entry.data_file );
884+ } else if (!ctx.columns_to_keep_stats .empty ()) {
885+ ContentFileUtil::DropUnselectedStats (*entry.data_file , ctx.columns_to_keep_stats );
886+ }
887+
888+ ICEBERG_ASSIGN_OR_RAISE (auto residual,
889+ ctx.residuals ->ResidualFor (entry.data_file ->partition ));
890+
891+ switch (entry.status ) {
892+ case ManifestStatus::kAdded :
893+ tasks.push_back (std::make_shared<AddedRowsScanTask>(
894+ change_ordinal, commit_snapshot_id, std::move (entry.data_file ),
895+ std::vector<std::shared_ptr<DataFile>>{}, std::move (residual)));
896+ break ;
897+ case ManifestStatus::kDeleted :
898+ tasks.push_back (std::make_shared<DeletedDataFileScanTask>(
899+ change_ordinal, commit_snapshot_id, std::move (entry.data_file ),
900+ std::vector<std::shared_ptr<DataFile>>{}, std::move (residual)));
901+ break ;
902+ case ManifestStatus::kExisting :
903+ return InvalidArgument (" Unexpected entry status: EXISTING" );
904+ }
905+ }
906+ return tasks;
907+ };
908+
909+ ICEBERG_ASSIGN_OR_RAISE (auto tasks, manifest_group->Plan (create_tasks_func));
910+ return tasks | std::views::transform ([](const auto & task) {
911+ return std::static_pointer_cast<ChangelogScanTask>(task);
912+ }) |
913+ std::ranges::to<std::vector>();
766914}
767915
768916} // namespace iceberg
0 commit comments