This is an automated email from the ASF dual-hosted git repository.

wgtmac pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-cpp.git


The following commit(s) were added to refs/heads/main by this push:
     new c712948f refactor: move each TableScan subclass to its own file (#994)
c712948f is described below

commit c712948f8cd29e14a3d83ae7070b3724579d34ac
Author: Manu Zhang <[email protected]>
AuthorDate: Sun Oct 11 21:19:38 2026 +0800

    refactor: move each TableScan subclass to its own file (#994)
    
    Move DataTableScan, IncrementalAppendScan and IncrementalChangelogScan
    into data_table_scan.{h,cc}, incremental_append_scan.{h,cc} and
    incremental_changelog_scan.{h,cc}. table_scan.{h,cc} keep TableScan,
    IncrementalScan, TableScanBuilder, the scan task types and
    TableScanContext.
    
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 example/demo_example.cc                            |   1 +
 src/iceberg/CMakeLists.txt                         |   3 +
 src/iceberg/cpp20_compatibility_internal.h         |   3 +
 src/iceberg/data_table_scan.cc                     | 220 +++++++++++
 src/iceberg/data_table_scan.h                      |  66 ++++
 src/iceberg/incremental_append_scan.cc             | 118 ++++++
 src/iceberg/incremental_append_scan.h              |  60 +++
 src/iceberg/incremental_changelog_scan.cc          | 183 +++++++++
 src/iceberg/incremental_changelog_scan.h           |  61 +++
 src/iceberg/table.cc                               |   3 +
 src/iceberg/table_scan.cc                          | 418 +--------------------
 src/iceberg/table_scan.h                           |  76 ----
 src/iceberg/test/incremental_append_scan_test.cc   |   2 +
 .../test/incremental_changelog_scan_test.cc        |   2 +
 src/iceberg/test/scan_planning_metrics_test.cc     |   1 +
 src/iceberg/test/table_scan_test.cc                |   1 +
 16 files changed, 731 insertions(+), 487 deletions(-)

diff --git a/example/demo_example.cc b/example/demo_example.cc
index 6098a98f..a965f9e8 100644
--- a/example/demo_example.cc
+++ b/example/demo_example.cc
@@ -23,6 +23,7 @@
 #include "iceberg/arrow/arrow_io_util.h"
 #include "iceberg/avro/avro_register.h"
 #include "iceberg/catalog/memory/in_memory_catalog.h"
+#include "iceberg/data_table_scan.h"
 #include "iceberg/manifest/manifest_entry.h"
 #include "iceberg/parquet/parquet_register.h"
 #include "iceberg/table.h"
diff --git a/src/iceberg/CMakeLists.txt b/src/iceberg/CMakeLists.txt
index 7394ce82..657d487c 100644
--- a/src/iceberg/CMakeLists.txt
+++ b/src/iceberg/CMakeLists.txt
@@ -26,6 +26,7 @@ set(ICEBERG_SOURCES
     catalog/memory/in_memory_catalog.cc
     catalog/session_catalog.cc
     catalog/session_context.cc
+    data_table_scan.cc
     delete_file_index.cc
     deletes/dv_util.cc
     deletes/dv_writer.cc
@@ -53,6 +54,8 @@ set(ICEBERG_SOURCES
     file_reader.cc
     file_writer.cc
     geospatial.cc
+    incremental_append_scan.cc
+    incremental_changelog_scan.cc
     inspect/history_table.cc
     inspect/metadata_table.cc
     inspect/snapshots_table.cc
diff --git a/src/iceberg/cpp20_compatibility_internal.h 
b/src/iceberg/cpp20_compatibility_internal.h
index 8c4a465f..b5f46e48 100644
--- a/src/iceberg/cpp20_compatibility_internal.h
+++ b/src/iceberg/cpp20_compatibility_internal.h
@@ -42,6 +42,7 @@
 #include "iceberg/data/file_scan_task_reader.h"
 #include "iceberg/data/position_delete_writer.h"
 #include "iceberg/data/writer.h"
+#include "iceberg/data_table_scan.h"
 #include "iceberg/delete_file_index.h"
 #include "iceberg/deletes/dv_writer.h"
 #include "iceberg/deletes/position_delete_index.h"
@@ -74,6 +75,8 @@
 #include "iceberg/iceberg_bundle_export.h"
 #include "iceberg/iceberg_data_export.h"
 #include "iceberg/iceberg_export.h"
+#include "iceberg/incremental_append_scan.h"
+#include "iceberg/incremental_changelog_scan.h"
 #include "iceberg/inheritable_metadata.h"
 #include "iceberg/inspect/history_table.h"
 #include "iceberg/inspect/metadata_table.h"
diff --git a/src/iceberg/data_table_scan.cc b/src/iceberg/data_table_scan.cc
new file mode 100644
index 00000000..f35f454c
--- /dev/null
+++ b/src/iceberg/data_table_scan.cc
@@ -0,0 +1,220 @@
+/*
+ * 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/data_table_scan.h"
+
+#include <algorithm>
+#include <chrono>
+#include <iterator>
+#include <optional>
+#include <utility>
+
+#include "iceberg/expression/sanitize_expression.h"
+#include "iceberg/manifest/manifest_group.h"
+#include "iceberg/metrics/metrics_context.h"
+#include "iceberg/metrics/metrics_reporter.h"
+#include "iceberg/metrics/scan_report.h"
+#include "iceberg/schema.h"
+#include "iceberg/snapshot.h"
+#include "iceberg/table_metadata.h"
+#include "iceberg/util/macros.h"
+#include "iceberg/util/type_util.h"
+
+namespace iceberg {
+
+namespace {
+
+template <typename T>
+class EmptyStream final : public Stream<T> {
+ public:
+  Result<std::optional<T>> NextImpl() override { return std::nullopt; }
+};
+
+Result<ScanReport> MakeScanReport(const DataTableScan& scan, const Snapshot& 
snapshot,
+                                  ScanMetricsResult scan_metrics) {
+  ICEBERG_ASSIGN_OR_RAISE(auto schema_ptr, scan.schema());
+
+  ICEBERG_ASSIGN_OR_RAISE(
+      auto projected_id_set,
+      GetProjectedIdsVisitor::GetProjectedIds(*schema_ptr, 
/*include_struct_ids=*/true));
+  std::vector<int32_t> projected_field_ids(projected_id_set.begin(),
+                                           projected_id_set.end());
+  std::ranges::sort(projected_field_ids);
+
+  std::vector<std::string> projected_field_names;
+  projected_field_names.reserve(projected_field_ids.size());
+  for (int32_t field_id : projected_field_ids) {
+    ICEBERG_ASSIGN_OR_RAISE(auto field_name, 
schema_ptr->FindColumnNameById(field_id));
+    ICEBERG_CHECK(field_name.has_value(), "Projected field {} not found in 
schema",
+                  field_id);
+    projected_field_names.emplace_back(*field_name);
+  }
+
+  ICEBERG_ASSIGN_OR_RAISE(auto sanitized_filter,
+                          SanitizeExpression::Sanitize(*schema_ptr, 
scan.filter(),
+                                                       
scan.context().case_sensitive));
+
+  return ScanReport{
+      .table_name = scan.context().table_name,
+      .snapshot_id = snapshot.snapshot_id,
+      .filter = std::move(sanitized_filter),
+      .schema_id = schema_ptr->schema_id(),
+      .projected_field_ids = std::move(projected_field_ids),
+      .projected_field_names = std::move(projected_field_names),
+      .scan_metrics = std::move(scan_metrics),
+      .metadata = scan.context().options,
+  };
+}
+
+class ReportingFileTaskStream final : public FileScanTaskStream {
+ public:
+  ReportingFileTaskStream(FileScanTaskStreamPtr stream,
+                          std::shared_ptr<ScanMetrics> scan_metrics,
+                          std::chrono::nanoseconds planning_duration,
+                          std::shared_ptr<MetricsReporter> reporter, 
ScanReport report)
+      : stream_(std::move(stream)),
+        scan_metrics_(std::move(scan_metrics)),
+        planning_duration_(std::move(planning_duration)),
+        reporter_(std::move(reporter)),
+        report_(std::move(report)) {}
+
+  ~ReportingFileTaskStream() override { Finalize(); }
+
+  Result<std::optional<std::shared_ptr<FileScanTask>>> NextImpl() override {
+    auto start = std::chrono::steady_clock::now();
+    auto result = stream_->Next();
+    planning_duration_ += std::chrono::duration_cast<std::chrono::nanoseconds>(
+        std::chrono::steady_clock::now() - start);
+    if (!result.has_value()) {
+      // Failed planning does not emit a successful scan report.
+      finalized_ = true;
+    } else if (!result.value().has_value()) {
+      Finalize();
+    }
+    return result;
+  }
+
+ private:
+  void Finalize() {
+    if (finalized_) {
+      return;
+    }
+    finalized_ = true;
+    scan_metrics_->total_planning_duration->Record(planning_duration_);
+    report_.scan_metrics = scan_metrics_->ToResult();
+    std::ignore = reporter_->Report(report_);
+  }
+
+  FileScanTaskStreamPtr stream_;
+  std::shared_ptr<ScanMetrics> scan_metrics_;
+  std::chrono::nanoseconds planning_duration_;
+  std::shared_ptr<MetricsReporter> reporter_;
+  ScanReport report_;
+  bool finalized_ = false;
+};
+
+}  // namespace
+
+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 stream, PlanFilesStream());
+  return stream->ToVector();
+}
+
+Result<FileScanTaskStreamPtr> DataTableScan::PlanFilesStream() const {
+  ICEBERG_ASSIGN_OR_RAISE(auto snapshot, this->snapshot());
+  if (!snapshot) {
+    return std::make_unique<EmptyStream<std::shared_ptr<FileScanTask>>>();
+  }
+
+  std::shared_ptr<ScanMetrics> scan_metrics;
+  std::optional<std::chrono::steady_clock::time_point> planning_start;
+  if (context_.metrics_reporter) {
+    auto metrics_context = MetricsContext::Default();
+    scan_metrics = ScanMetrics::Make(*metrics_context);
+    planning_start = std::chrono::steady_clock::now();
+  }
+
+  TableMetadataCache metadata_cache(metadata_.get());
+  ICEBERG_ASSIGN_OR_RAISE(auto specs_by_id, 
metadata_cache.GetPartitionSpecsById());
+
+  SnapshotReader snapshot_reader(snapshot.get());
+  ICEBERG_ASSIGN_OR_RAISE(auto data_manifests, 
snapshot_reader.DataManifests(io_));
+  ICEBERG_ASSIGN_OR_RAISE(auto delete_manifests, 
snapshot_reader.DeleteManifests(io_));
+
+  if (scan_metrics) {
+    scan_metrics->total_data_manifests->Increment(
+        static_cast<int64_t>(data_manifests.size()));
+    scan_metrics->total_delete_manifests->Increment(
+        static_cast<int64_t>(delete_manifests.size()));
+  }
+
+  std::vector<ManifestFile> owned_data_manifests(
+      std::make_move_iterator(data_manifests.begin()),
+      std::make_move_iterator(data_manifests.end()));
+  std::vector<ManifestFile> owned_delete_manifests(
+      std::make_move_iterator(delete_manifests.begin()),
+      std::make_move_iterator(delete_manifests.end()));
+
+  ICEBERG_ASSIGN_OR_RAISE(
+      auto manifest_group,
+      ManifestGroup::Make(io_, schema_, specs_by_id, 
std::move(owned_data_manifests),
+                          std::move(owned_delete_manifests)));
+  manifest_group->CaseSensitive(context_.case_sensitive)
+      .Select(ScanColumns())
+      .FilterData(filter())
+      .IgnoreDeleted()
+      .ColumnsToKeepStats(context_.columns_to_keep_stats)
+      .WithScanMetrics(scan_metrics);
+  if (data_manifests.size() > 1 || delete_manifests.size() > 1) {
+    manifest_group->PlanWith(context_.plan_executor);
+  }
+  if (context_.ignore_residuals) {
+    manifest_group->IgnoreResiduals();
+  }
+
+  ICEBERG_ASSIGN_OR_RAISE(auto stream, 
std::move(*manifest_group).PlanFilesStream());
+  if (!planning_start.has_value()) {
+    return stream;
+  }
+
+  auto planning_duration = 
std::chrono::duration_cast<std::chrono::nanoseconds>(
+      std::chrono::steady_clock::now() - planning_start.value());
+
+  auto report = MakeScanReport(*this, *snapshot, ScanMetricsResult{});
+  if (!report.has_value()) {
+    // Scan reporting is best effort.
+    return stream;
+  }
+
+  return std::make_unique<ReportingFileTaskStream>(
+      std::move(stream), std::move(scan_metrics), planning_duration,
+      context_.metrics_reporter, std::move(report).value());
+}
+
+}  // namespace iceberg
diff --git a/src/iceberg/data_table_scan.h b/src/iceberg/data_table_scan.h
new file mode 100644
index 00000000..a6de0020
--- /dev/null
+++ b/src/iceberg/data_table_scan.h
@@ -0,0 +1,66 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+/// \file iceberg/data_table_scan.h
+/// \brief Scan that reads the data files of a table snapshot.
+
+#include <memory>
+#include <vector>
+
+#include "iceberg/iceberg_export.h"
+#include "iceberg/result.h"
+#include "iceberg/table_scan.h"
+#include "iceberg/type_fwd.h"
+
+namespace iceberg {
+
+/// \brief A scan that reads data files and applies delete files to filter 
rows.
+class ICEBERG_EXPORT DataTableScan : public TableScan {
+ public:
+  ~DataTableScan() override = default;
+
+  /// \brief Constructs a DataTableScan instance.
+  static Result<std::unique_ptr<DataTableScan>> Make(
+      std::shared_ptr<TableMetadata> metadata, std::shared_ptr<Schema> schema,
+      std::shared_ptr<FileIO> io, internal::TableScanContext context);
+
+  /// \brief Plans the scan tasks by resolving manifests and data files.
+  ///
+  /// Collects PlanFilesStream() into a vector.
+  /// \return A Result containing scan tasks or an error.
+  Result<std::vector<std::shared_ptr<FileScanTask>>> PlanFiles() const;
+
+  /// \brief Lazily plans scan tasks by resolving manifests and data files on 
demand.
+  ///
+  /// The returned fallible, single-pass stream owns its planning resources and
+  /// can outlive this scan. An executor configured through PlanWith() is 
borrowed and
+  /// must remain alive until the stream is destroyed, as later Next() calls 
may submit
+  /// work to it.
+  Result<FileScanTaskStreamPtr> PlanFilesStream() const;
+
+ protected:
+  using TableScan::TableScan;
+};
+
+extern template class ICEBERG_EXTERN_TEMPLATE_CLASS_EXPORT
+    TableScanBuilder<DataTableScan>;
+
+}  // namespace iceberg
diff --git a/src/iceberg/incremental_append_scan.cc 
b/src/iceberg/incremental_append_scan.cc
new file mode 100644
index 00000000..cf01fe89
--- /dev/null
+++ b/src/iceberg/incremental_append_scan.cc
@@ -0,0 +1,118 @@
+/*
+ * 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/incremental_append_scan.h"
+
+#include <algorithm>
+#include <iterator>
+#include <unordered_set>
+#include <utility>
+
+#include "iceberg/manifest/manifest_entry.h"
+#include "iceberg/manifest/manifest_group.h"
+#include "iceberg/snapshot.h"
+#include "iceberg/table_metadata.h"
+#include "iceberg/util/macros.h"
+#include "iceberg/util/snapshot_util.h"
+
+namespace iceberg {
+
+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) {
+    SnapshotReader snapshot_reader(snapshot.get());
+    ICEBERG_ASSIGN_OR_RAISE(auto manifests, 
snapshot_reader.DataManifests(io_));
+    std::ranges::copy_if(
+        manifests, std::inserter(data_manifests, data_manifests.end()),
+        [&snapshot_ids](const ManifestFile& manifest) {
+          return manifest.added_snapshot_id.has_value() &&
+                 snapshot_ids.contains(manifest.added_snapshot_id.value());
+        });
+  }
+  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 std::move(*manifest_group).PlanFiles();
+}
+
+}  // namespace iceberg
diff --git a/src/iceberg/incremental_append_scan.h 
b/src/iceberg/incremental_append_scan.h
new file mode 100644
index 00000000..74922d27
--- /dev/null
+++ b/src/iceberg/incremental_append_scan.h
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+/// \file iceberg/incremental_append_scan.h
+/// \brief Scan that reads the data files appended between snapshots.
+
+#include <cstdint>
+#include <memory>
+#include <optional>
+#include <vector>
+
+#include "iceberg/iceberg_export.h"
+#include "iceberg/result.h"
+#include "iceberg/table_scan.h"
+#include "iceberg/type_fwd.h"
+
+namespace iceberg {
+
+/// \brief A scan that reads data files added between snapshots (incremental 
appends).
+class ICEBERG_EXPORT IncrementalAppendScan : public 
IncrementalScan<FileScanTask> {
+ public:
+  /// \brief Constructs an IncrementalAppendScan instance.
+  static Result<std::unique_ptr<IncrementalAppendScan>> Make(
+      std::shared_ptr<TableMetadata> metadata, std::shared_ptr<Schema> schema,
+      std::shared_ptr<FileIO> io, internal::TableScanContext context);
+
+  ~IncrementalAppendScan() override = default;
+
+  Result<std::vector<std::shared_ptr<FileScanTask>>> PlanFiles() const 
override;
+
+ protected:
+  Result<std::vector<std::shared_ptr<FileScanTask>>> PlanFiles(
+      std::optional<int64_t> from_snapshot_id_exclusive,
+      int64_t to_snapshot_id_inclusive) const override;
+
+  using IncrementalScan::IncrementalScan;
+};
+
+extern template class ICEBERG_EXTERN_TEMPLATE_CLASS_EXPORT
+    TableScanBuilder<IncrementalAppendScan>;
+
+}  // namespace iceberg
diff --git a/src/iceberg/incremental_changelog_scan.cc 
b/src/iceberg/incremental_changelog_scan.cc
new file mode 100644
index 00000000..5a1dbba1
--- /dev/null
+++ b/src/iceberg/incremental_changelog_scan.cc
@@ -0,0 +1,183 @@
+/*
+ * 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/incremental_changelog_scan.h"
+
+#include <limits>
+#include <ranges>
+#include <unordered_map>
+#include <unordered_set>
+#include <utility>
+
+#include "iceberg/expression/residual_evaluator.h"
+#include "iceberg/manifest/manifest_entry.h"
+#include "iceberg/manifest/manifest_group.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.h"
+
+namespace iceberg {
+
+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<SnapshotReader>>>
+      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_reader = std::make_unique<SnapshotReader>(snapshot.get());
+      ICEBERG_ASSIGN_OR_RAISE(auto delete_manifests,
+                              snapshot_reader->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_reader));
+    }
+  }
+  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 (manifest.added_snapshot_id.has_value() &&
+          snapshot_ids.contains(manifest.added_snapshot_id.value()) &&
+          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
diff --git a/src/iceberg/incremental_changelog_scan.h 
b/src/iceberg/incremental_changelog_scan.h
new file mode 100644
index 00000000..6625b68f
--- /dev/null
+++ b/src/iceberg/incremental_changelog_scan.h
@@ -0,0 +1,61 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+/// \file iceberg/incremental_changelog_scan.h
+/// \brief Scan that reads the changelog between snapshots.
+
+#include <cstdint>
+#include <memory>
+#include <optional>
+#include <vector>
+
+#include "iceberg/iceberg_export.h"
+#include "iceberg/result.h"
+#include "iceberg/table_scan.h"
+#include "iceberg/type_fwd.h"
+
+namespace iceberg {
+
+/// \brief A scan that reads changelog entries between snapshots.
+class ICEBERG_EXPORT IncrementalChangelogScan
+    : public IncrementalScan<ChangelogScanTask> {
+ public:
+  /// \brief Constructs an IncrementalChangelogScan instance.
+  static Result<std::unique_ptr<IncrementalChangelogScan>> Make(
+      std::shared_ptr<TableMetadata> metadata, std::shared_ptr<Schema> schema,
+      std::shared_ptr<FileIO> io, internal::TableScanContext context);
+
+  ~IncrementalChangelogScan() override = default;
+
+  Result<std::vector<std::shared_ptr<ChangelogScanTask>>> PlanFiles() const 
override;
+
+ protected:
+  Result<std::vector<std::shared_ptr<ChangelogScanTask>>> PlanFiles(
+      std::optional<int64_t> from_snapshot_id_exclusive,
+      int64_t to_snapshot_id_inclusive) const override;
+
+  using IncrementalScan::IncrementalScan;
+};
+
+extern template class ICEBERG_EXTERN_TEMPLATE_CLASS_EXPORT
+    TableScanBuilder<IncrementalChangelogScan>;
+
+}  // namespace iceberg
diff --git a/src/iceberg/table.cc b/src/iceberg/table.cc
index c512c11d..ca02c623 100644
--- a/src/iceberg/table.cc
+++ b/src/iceberg/table.cc
@@ -22,6 +22,9 @@
 #include <memory>
 
 #include "iceberg/catalog.h"
+#include "iceberg/data_table_scan.h"
+#include "iceberg/incremental_append_scan.h"
+#include "iceberg/incremental_changelog_scan.h"
 #include "iceberg/location_provider.h"
 #include "iceberg/partition_spec.h"
 #include "iceberg/result.h"
diff --git a/src/iceberg/table_scan.cc b/src/iceberg/table_scan.cc
index f6023a6f..46f235fb 100644
--- a/src/iceberg/table_scan.cc
+++ b/src/iceberg/table_scan.cc
@@ -20,27 +20,23 @@
 #include "iceberg/table_scan.h"
 
 #include <algorithm>
-#include <chrono>
 #include <cstdint>
 #include <iterator>
 #include <ranges>
 #include <utility>
 
+#include "iceberg/data_table_scan.h"
 #include "iceberg/expression/binder.h"
 #include "iceberg/expression/expression.h"
-#include "iceberg/expression/residual_evaluator.h"
-#include "iceberg/expression/sanitize_expression.h"
+#include "iceberg/incremental_append_scan.h"
+#include "iceberg/incremental_changelog_scan.h"
 #include "iceberg/manifest/manifest_entry.h"
-#include "iceberg/manifest/manifest_group.h"
-#include "iceberg/metrics/metrics_context.h"
 #include "iceberg/metrics/metrics_reporters.h"
-#include "iceberg/metrics/scan_report.h"
 #include "iceberg/result.h"
 #include "iceberg/schema.h"
 #include "iceberg/snapshot.h"
 #include "iceberg/table.h"
 #include "iceberg/table_metadata.h"
-#include "iceberg/util/content_file_util.h"
 #include "iceberg/util/macros.h"
 #include "iceberg/util/snapshot_util.h"
 #include "iceberg/util/timepoint.h"
@@ -67,95 +63,6 @@ const std::vector<std::string> kScanColumnsWithStats = [] {
   return cols;
 }();
 
-template <typename T>
-class EmptyStream final : public Stream<T> {
- public:
-  Result<std::optional<T>> NextImpl() override { return std::nullopt; }
-};
-
-Result<ScanReport> MakeScanReport(const DataTableScan& scan, const Snapshot& 
snapshot,
-                                  ScanMetricsResult scan_metrics) {
-  ICEBERG_ASSIGN_OR_RAISE(auto schema_ptr, scan.schema());
-
-  ICEBERG_ASSIGN_OR_RAISE(
-      auto projected_id_set,
-      GetProjectedIdsVisitor::GetProjectedIds(*schema_ptr, 
/*include_struct_ids=*/true));
-  std::vector<int32_t> projected_field_ids(projected_id_set.begin(),
-                                           projected_id_set.end());
-  std::ranges::sort(projected_field_ids);
-
-  std::vector<std::string> projected_field_names;
-  projected_field_names.reserve(projected_field_ids.size());
-  for (int32_t field_id : projected_field_ids) {
-    ICEBERG_ASSIGN_OR_RAISE(auto field_name, 
schema_ptr->FindColumnNameById(field_id));
-    ICEBERG_CHECK(field_name.has_value(), "Projected field {} not found in 
schema",
-                  field_id);
-    projected_field_names.emplace_back(*field_name);
-  }
-
-  ICEBERG_ASSIGN_OR_RAISE(auto sanitized_filter,
-                          SanitizeExpression::Sanitize(*schema_ptr, 
scan.filter(),
-                                                       
scan.context().case_sensitive));
-
-  return ScanReport{
-      .table_name = scan.context().table_name,
-      .snapshot_id = snapshot.snapshot_id,
-      .filter = std::move(sanitized_filter),
-      .schema_id = schema_ptr->schema_id(),
-      .projected_field_ids = std::move(projected_field_ids),
-      .projected_field_names = std::move(projected_field_names),
-      .scan_metrics = std::move(scan_metrics),
-      .metadata = scan.context().options,
-  };
-}
-
-class ReportingFileTaskStream final : public FileScanTaskStream {
- public:
-  ReportingFileTaskStream(FileScanTaskStreamPtr stream,
-                          std::shared_ptr<ScanMetrics> scan_metrics,
-                          std::chrono::nanoseconds planning_duration,
-                          std::shared_ptr<MetricsReporter> reporter, 
ScanReport report)
-      : stream_(std::move(stream)),
-        scan_metrics_(std::move(scan_metrics)),
-        planning_duration_(std::move(planning_duration)),
-        reporter_(std::move(reporter)),
-        report_(std::move(report)) {}
-
-  ~ReportingFileTaskStream() override { Finalize(); }
-
-  Result<std::optional<std::shared_ptr<FileScanTask>>> NextImpl() override {
-    auto start = std::chrono::steady_clock::now();
-    auto result = stream_->Next();
-    planning_duration_ += std::chrono::duration_cast<std::chrono::nanoseconds>(
-        std::chrono::steady_clock::now() - start);
-    if (!result.has_value()) {
-      // Failed planning does not emit a successful scan report.
-      finalized_ = true;
-    } else if (!result.value().has_value()) {
-      Finalize();
-    }
-    return result;
-  }
-
- private:
-  void Finalize() {
-    if (finalized_) {
-      return;
-    }
-    finalized_ = true;
-    scan_metrics_->total_planning_duration->Record(planning_duration_);
-    report_.scan_metrics = scan_metrics_->ToResult();
-    std::ignore = reporter_->Report(report_);
-  }
-
-  FileScanTaskStreamPtr stream_;
-  std::shared_ptr<ScanMetrics> scan_metrics_;
-  std::chrono::nanoseconds planning_duration_;
-  std::shared_ptr<MetricsReporter> reporter_;
-  ScanReport report_;
-  bool finalized_ = false;
-};
-
 }  // namespace
 
 namespace internal {
@@ -650,92 +557,6 @@ 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 stream, PlanFilesStream());
-  return stream->ToVector();
-}
-
-Result<FileScanTaskStreamPtr> DataTableScan::PlanFilesStream() const {
-  ICEBERG_ASSIGN_OR_RAISE(auto snapshot, this->snapshot());
-  if (!snapshot) {
-    return std::make_unique<EmptyStream<std::shared_ptr<FileScanTask>>>();
-  }
-
-  std::shared_ptr<ScanMetrics> scan_metrics;
-  std::optional<std::chrono::steady_clock::time_point> planning_start;
-  if (context_.metrics_reporter) {
-    auto metrics_context = MetricsContext::Default();
-    scan_metrics = ScanMetrics::Make(*metrics_context);
-    planning_start = std::chrono::steady_clock::now();
-  }
-
-  TableMetadataCache metadata_cache(metadata_.get());
-  ICEBERG_ASSIGN_OR_RAISE(auto specs_by_id, 
metadata_cache.GetPartitionSpecsById());
-
-  SnapshotReader snapshot_reader(snapshot.get());
-  ICEBERG_ASSIGN_OR_RAISE(auto data_manifests, 
snapshot_reader.DataManifests(io_));
-  ICEBERG_ASSIGN_OR_RAISE(auto delete_manifests, 
snapshot_reader.DeleteManifests(io_));
-
-  if (scan_metrics) {
-    scan_metrics->total_data_manifests->Increment(
-        static_cast<int64_t>(data_manifests.size()));
-    scan_metrics->total_delete_manifests->Increment(
-        static_cast<int64_t>(delete_manifests.size()));
-  }
-
-  std::vector<ManifestFile> owned_data_manifests(
-      std::make_move_iterator(data_manifests.begin()),
-      std::make_move_iterator(data_manifests.end()));
-  std::vector<ManifestFile> owned_delete_manifests(
-      std::make_move_iterator(delete_manifests.begin()),
-      std::make_move_iterator(delete_manifests.end()));
-
-  ICEBERG_ASSIGN_OR_RAISE(
-      auto manifest_group,
-      ManifestGroup::Make(io_, schema_, specs_by_id, 
std::move(owned_data_manifests),
-                          std::move(owned_delete_manifests)));
-  manifest_group->CaseSensitive(context_.case_sensitive)
-      .Select(ScanColumns())
-      .FilterData(filter())
-      .IgnoreDeleted()
-      .ColumnsToKeepStats(context_.columns_to_keep_stats)
-      .WithScanMetrics(scan_metrics);
-  if (data_manifests.size() > 1 || delete_manifests.size() > 1) {
-    manifest_group->PlanWith(context_.plan_executor);
-  }
-  if (context_.ignore_residuals) {
-    manifest_group->IgnoreResiduals();
-  }
-
-  ICEBERG_ASSIGN_OR_RAISE(auto stream, 
std::move(*manifest_group).PlanFilesStream());
-  if (!planning_start.has_value()) {
-    return stream;
-  }
-
-  auto planning_duration = 
std::chrono::duration_cast<std::chrono::nanoseconds>(
-      std::chrono::steady_clock::now() - planning_start.value());
-
-  auto report = MakeScanReport(*this, *snapshot, ScanMetricsResult{});
-  if (!report.has_value()) {
-    // Scan reporting is best effort.
-    return stream;
-  }
-
-  return std::make_unique<ReportingFileTaskStream>(
-      std::move(stream), std::move(scan_metrics), planning_duration,
-      context_.metrics_reporter, std::move(report).value());
-}
-
 // 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.
@@ -760,234 +581,9 @@ Result<std::vector<std::shared_ptr<ScanTaskType>>> 
ResolvePlanFiles(
   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) {
-    SnapshotReader snapshot_reader(snapshot.get());
-    ICEBERG_ASSIGN_OR_RAISE(auto manifests, 
snapshot_reader.DataManifests(io_));
-    std::ranges::copy_if(
-        manifests, std::inserter(data_manifests, data_manifests.end()),
-        [&snapshot_ids](const ManifestFile& manifest) {
-          return manifest.added_snapshot_id.has_value() &&
-                 snapshot_ids.contains(manifest.added_snapshot_id.value());
-        });
-  }
-  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 std::move(*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<SnapshotReader>>>
-      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_reader = std::make_unique<SnapshotReader>(snapshot.get());
-      ICEBERG_ASSIGN_OR_RAISE(auto delete_manifests,
-                              snapshot_reader->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_reader));
-    }
-  }
-  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 (manifest.added_snapshot_id.has_value() &&
-          snapshot_ids.contains(manifest.added_snapshot_id.value()) &&
-          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>();
-}
+template Result<std::vector<std::shared_ptr<FileScanTask>>> ResolvePlanFiles(
+    const IncrementalScan<FileScanTask>& scan);
+template Result<std::vector<std::shared_ptr<ChangelogScanTask>>> 
ResolvePlanFiles(
+    const IncrementalScan<ChangelogScanTask>& scan);
 
 }  // namespace iceberg
diff --git a/src/iceberg/table_scan.h b/src/iceberg/table_scan.h
index 7310d435..6ec29ad9 100644
--- a/src/iceberg/table_scan.h
+++ b/src/iceberg/table_scan.h
@@ -462,34 +462,6 @@ class ICEBERG_EXPORT TableScan {
   mutable std::shared_ptr<Schema> projected_schema_;
 };
 
-/// \brief A scan that reads data files and applies delete files to filter 
rows.
-class ICEBERG_EXPORT DataTableScan : public TableScan {
- public:
-  ~DataTableScan() override = default;
-
-  /// \brief Constructs a DataTableScan instance.
-  static Result<std::unique_ptr<DataTableScan>> Make(
-      std::shared_ptr<TableMetadata> metadata, std::shared_ptr<Schema> schema,
-      std::shared_ptr<FileIO> io, internal::TableScanContext context);
-
-  /// \brief Plans the scan tasks by resolving manifests and data files.
-  ///
-  /// Collects PlanFilesStream() into a vector.
-  /// \return A Result containing scan tasks or an error.
-  Result<std::vector<std::shared_ptr<FileScanTask>>> PlanFiles() const;
-
-  /// \brief Lazily plans scan tasks by resolving manifests and data files on 
demand.
-  ///
-  /// The returned fallible, single-pass stream owns its planning resources and
-  /// can outlive this scan. An executor configured through PlanWith() is 
borrowed and
-  /// must remain alive until the stream is destroyed, as later Next() calls 
may submit
-  /// work to it.
-  Result<FileScanTaskStreamPtr> PlanFilesStream() const;
-
- protected:
-  using TableScan::TableScan;
-};
-
 /// \brief A base template class for incremental scans that read changes 
between
 /// snapshots, and return scan tasks of the specified type.
 template <typename ScanTaskType>
@@ -512,52 +484,4 @@ class IncrementalScan : public TableScan {
       const IncrementalScan<T>& scan);
 };
 
-/// \brief A scan that reads data files added between snapshots (incremental 
appends).
-class ICEBERG_EXPORT IncrementalAppendScan : public 
IncrementalScan<FileScanTask> {
- public:
-  /// \brief Constructs an IncrementalAppendScan instance.
-  static Result<std::unique_ptr<IncrementalAppendScan>> Make(
-      std::shared_ptr<TableMetadata> metadata, std::shared_ptr<Schema> schema,
-      std::shared_ptr<FileIO> io, internal::TableScanContext context);
-
-  ~IncrementalAppendScan() override = default;
-
-  Result<std::vector<std::shared_ptr<FileScanTask>>> PlanFiles() const 
override;
-
- protected:
-  Result<std::vector<std::shared_ptr<FileScanTask>>> PlanFiles(
-      std::optional<int64_t> from_snapshot_id_exclusive,
-      int64_t to_snapshot_id_inclusive) const override;
-
-  using IncrementalScan::IncrementalScan;
-};
-
-/// \brief A scan that reads changelog entries between snapshots.
-class ICEBERG_EXPORT IncrementalChangelogScan
-    : public IncrementalScan<ChangelogScanTask> {
- public:
-  /// \brief Constructs an IncrementalChangelogScan instance.
-  static Result<std::unique_ptr<IncrementalChangelogScan>> Make(
-      std::shared_ptr<TableMetadata> metadata, std::shared_ptr<Schema> schema,
-      std::shared_ptr<FileIO> io, internal::TableScanContext context);
-
-  ~IncrementalChangelogScan() override = default;
-
-  Result<std::vector<std::shared_ptr<ChangelogScanTask>>> PlanFiles() const 
override;
-
- protected:
-  Result<std::vector<std::shared_ptr<ChangelogScanTask>>> PlanFiles(
-      std::optional<int64_t> from_snapshot_id_exclusive,
-      int64_t to_snapshot_id_inclusive) const override;
-
-  using IncrementalScan::IncrementalScan;
-};
-
-extern template class ICEBERG_EXTERN_TEMPLATE_CLASS_EXPORT
-    TableScanBuilder<DataTableScan>;
-extern template class ICEBERG_EXTERN_TEMPLATE_CLASS_EXPORT
-    TableScanBuilder<IncrementalAppendScan>;
-extern template class ICEBERG_EXTERN_TEMPLATE_CLASS_EXPORT
-    TableScanBuilder<IncrementalChangelogScan>;
-
 }  // namespace iceberg
diff --git a/src/iceberg/test/incremental_append_scan_test.cc 
b/src/iceberg/test/incremental_append_scan_test.cc
index 194b5115..90cdc8f5 100644
--- a/src/iceberg/test/incremental_append_scan_test.cc
+++ b/src/iceberg/test/incremental_append_scan_test.cc
@@ -17,6 +17,8 @@
  * under the License.
  */
 
+#include "iceberg/incremental_append_scan.h"
+
 #include <memory>
 #include <optional>
 #include <string>
diff --git a/src/iceberg/test/incremental_changelog_scan_test.cc 
b/src/iceberg/test/incremental_changelog_scan_test.cc
index dbf71537..e8b37404 100644
--- a/src/iceberg/test/incremental_changelog_scan_test.cc
+++ b/src/iceberg/test/incremental_changelog_scan_test.cc
@@ -17,6 +17,8 @@
  * under the License.
  */
 
+#include "iceberg/incremental_changelog_scan.h"
+
 #include <memory>
 #include <optional>
 #include <string>
diff --git a/src/iceberg/test/scan_planning_metrics_test.cc 
b/src/iceberg/test/scan_planning_metrics_test.cc
index 9098ccde..000f0e59 100644
--- a/src/iceberg/test/scan_planning_metrics_test.cc
+++ b/src/iceberg/test/scan_planning_metrics_test.cc
@@ -26,6 +26,7 @@
 #include <gmock/gmock.h>
 #include <gtest/gtest.h>
 
+#include "iceberg/data_table_scan.h"
 #include "iceberg/expression/expressions.h"
 #include "iceberg/manifest/manifest_entry.h"
 #include "iceberg/metrics/metrics_reporter.h"
diff --git a/src/iceberg/test/table_scan_test.cc 
b/src/iceberg/test/table_scan_test.cc
index c9af765e..f08440a1 100644
--- a/src/iceberg/test/table_scan_test.cc
+++ b/src/iceberg/test/table_scan_test.cc
@@ -27,6 +27,7 @@
 #include <gmock/gmock.h>
 #include <gtest/gtest.h>
 
+#include "iceberg/data_table_scan.h"
 #include "iceberg/expression/expressions.h"
 #include "iceberg/snapshot.h"
 #include "iceberg/table_metadata.h"

Reply via email to