Copilot commented on code in PR #873:
URL: https://github.com/apache/iceberg-cpp/pull/873#discussion_r3985353805


##########
src/iceberg/manifest/manifest_group.cc:
##########
@@ -131,6 +131,310 @@ ManifestGroup::~ManifestGroup() = default;
 ManifestGroup::ManifestGroup(ManifestGroup&&) noexcept = default;
 ManifestGroup& ManifestGroup::operator=(ManifestGroup&&) noexcept = default;
 
+class ManifestGroup::FilePlanningStream final : public FileScanTaskStream {
+ public:
+  static Result<FileScanTaskStreamPtr> Make(std::unique_ptr<ManifestGroup> 
group) {
+    ICEBERG_RETURN_UNEXPECTED(group->CheckErrors());
+
+    group->delete_index_builder_.WithScanMetrics(group->scan_metrics_);
+    ICEBERG_ASSIGN_OR_RAISE(auto delete_index, 
group->delete_index_builder_.Build());
+
+    auto stats_projection =
+        group->PrepareStatsProjection(delete_index->has_equality_deletes());
+
+    std::unique_ptr<Evaluator> data_file_evaluator;
+    if (group->file_filter_ &&
+        group->file_filter_->op() != Expression::Operation::kTrue) {
+      ICEBERG_ASSIGN_OR_RAISE(
+          data_file_evaluator,
+          Evaluator::Make(*DataFileFilterSchema(), group->file_filter_,
+                          group->case_sensitive_));
+    }
+    const bool drop_stats = stats_projection.drop_stats;
+
+    return FileScanTaskStreamPtr(new FilePlanningStream(
+        std::move(group), std::move(delete_index), 
std::move(data_file_evaluator),
+        std::move(stats_projection.columns), drop_stats));
+  }
+
+  Result<std::optional<std::shared_ptr<FileScanTask>>> NextImpl() override {
+    while (true) {
+      ICEBERG_ASSIGN_OR_RAISE(auto entry, NextEntry());
+      if (!entry.has_value()) {
+        return std::nullopt;
+      }
+
+      auto [spec_id, value] = std::move(entry).value();
+      if (group_->ignore_existing_ && value.status == 
ManifestStatus::kExisting) {
+        IncrementSkippedDataFiles();
+        continue;
+      }
+
+      ICEBERG_DCHECK(value.data_file != nullptr, "Data file cannot be null");
+      if (data_file_evaluator_) {
+        DataFileStructLike data_file(*value.data_file);
+        ICEBERG_ASSIGN_OR_RAISE(bool should_match,
+                                data_file_evaluator_->Evaluate(data_file));
+        if (!should_match) {
+          IncrementSkippedDataFiles();
+          continue;
+        }
+      }
+
+      if (!group_->manifest_entry_predicate_(value)) {
+        IncrementSkippedDataFiles();
+        continue;
+      }
+
+      ICEBERG_ASSIGN_OR_RAISE(auto delete_files, 
delete_index_->ForEntry(value));
+
+      // Equality-delete matching uses data-file statistics. Drop unrequested 
stats only
+      // after the delete index has finished matching this entry.
+      if (drop_stats_) {
+        ContentFileUtil::DropAllStats(*value.data_file);
+      } else if (!group_->columns_to_keep_stats_.empty()) {
+        ContentFileUtil::DropUnselectedStats(*value.data_file,
+                                             group_->columns_to_keep_stats_);
+      }
+
+      UpdateResultMetrics(*value.data_file, delete_files);
+
+      ICEBERG_ASSIGN_OR_RAISE(auto residuals, GetResidualEvaluator(spec_id));
+      ICEBERG_ASSIGN_OR_RAISE(auto residual,
+                              
residuals->ResidualFor(value.data_file->partition));
+
+      return 
std::optional<std::shared_ptr<FileScanTask>>{std::make_shared<FileScanTask>(
+          std::move(value.data_file), std::move(delete_files), 
std::move(residual))};
+    }
+  }
+
+ private:
+  FilePlanningStream(std::unique_ptr<ManifestGroup> group,
+                     std::unique_ptr<DeleteFileIndex> delete_index,
+                     std::unique_ptr<Evaluator> data_file_evaluator,
+                     std::vector<std::string> columns, bool drop_stats)
+      : group_(std::move(group)),
+        delete_index_(std::move(delete_index)),
+        data_file_evaluator_(std::move(data_file_evaluator)),
+        columns_(std::move(columns)),
+        drop_stats_(drop_stats) {}
+
+  using TaggedEntry = std::pair<int32_t, ManifestEntry>;
+  using TaggedStream = std::pair<int32_t, ManifestEntryStreamPtr>;
+
+  Result<std::optional<TaggedEntry>> NextEntry() {
+    if (!group_->executor_.has_value()) {
+      while (true) {
+        if (!entry_stream_) {
+          ICEBERG_ASSIGN_OR_RAISE(bool opened, OpenNextManifest());
+          if (!opened) {
+            return std::nullopt;
+          }
+        }
+
+        ICEBERG_ASSIGN_OR_RAISE(auto entry, entry_stream_->Next());
+        if (!entry.has_value()) {
+          entry_stream_.reset();
+          continue;
+        }
+        return std::optional<TaggedEntry>{std::in_place, current_spec_id_,
+                                          std::move(entry).value()};
+      }
+    }
+
+    while (true) {
+      if (next_batch_stream_ == batch_streams_.size()) {
+        ICEBERG_ASSIGN_OR_RAISE(bool loaded, LoadNextManifestBatch());
+        if (!loaded) {
+          return std::nullopt;
+        }
+      }

Review Comment:
   When executor planning is enabled, `LoadNextManifestBatch()` returns `false` 
when the current 32-manifest window contains no eligible manifests. This branch 
treats that as end-of-stream, so if the first 32 manifests are filtered out but 
a later manifest matches, the stream silently returns no tasks. Continue 
loading batches until an eligible manifest is found or `next_manifest_` reaches 
the end.



##########
src/iceberg/manifest/manifest_group.h:
##########
@@ -136,6 +137,18 @@ class ICEBERG_EXPORT ManifestGroup : public ErrorCollector 
{
   /// \brief Plan scan tasks for all matching data files.
   Result<std::vector<std::shared_ptr<FileScanTask>>> PlanFiles();
 
+  /// \brief Lazily plan scan tasks for matching data files.
+  ///
+  /// The returned stream owns the planning state and may outlive this 
ManifestGroup.
+  /// It reads one bounded manifest batch at a time instead of materializing 
all manifest
+  /// entries and scan tasks. When PlanWith() configures an executor, entry 
streams for
+  /// manifests in each batch are opened in parallel, while entries are 
consumed one
+  /// manifest at a time. Delete manifests are still read eagerly when 
creating the
+  /// stream because delete files must be indexed before data-file planning 
can begin.
+  /// Creating the stream consumes this group's configuration, so this method 
may only
+  /// be called on an rvalue.

Review Comment:
   `PlanWith()` stores only a non-owning `std::reference_wrapper<Executor>`, 
and the moved group retains that reference for later `Next()` calls. The stream 
is documented as outliving this group/scan, but consuming it after a locally 
scoped executor is destroyed will dereference a dangling reference. Either 
retain an owned executor lifetime or document/enforce that a configured 
executor must remain alive until the stream is destroyed.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to