Skip to content

Commit a51089d

Browse files
manuzhangcodex
andcommitted
fix: retain partition values during scan planning
Preserve partitions for delete matching with narrow projections and cover equality and position deletes. Document the intentional stream API migrations. Co-authored-by: Codex <codex@openai.com>
1 parent 5c20a07 commit a51089d

5 files changed

Lines changed: 96 additions & 12 deletions

File tree

‎src/iceberg/manifest/manifest_group.cc‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,13 @@ ManifestGroup::StatsProjection ManifestGroup::PrepareStatsProjection(
645645
// here ensures eager and stream planning use identical semantics.
646646
StatsProjection result{.columns = columns_,
647647
.drop_stats = ManifestReader::ShouldDropStats(columns_)};
648+
// Delete matching and residual evaluation require partition values even when the
649+
// caller does not select them. Do not narrow an empty or select-all projection.
650+
if (!result.columns.empty() &&
651+
!std::ranges::contains(result.columns, Schema::kAllColumns) &&
652+
!std::ranges::contains(result.columns, DataFile::kPartitionField)) {
653+
result.columns.emplace_back(DataFile::kPartitionField);
654+
}
648655
if (has_equality_deletes) {
649656
result.columns = ManifestReader::WithStatsColumns(result.columns);
650657
}

‎src/iceberg/manifest/manifest_group.h‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ class ICEBERG_EXPORT ManifestGroup : public ErrorCollector {
114114

115115
/// \brief Select specific columns from manifest entries.
116116
///
117+
/// Task planning also reads partition values for delete matching and residuals, and
118+
/// may temporarily read statistics needed for equality-delete matching.
119+
///
117120
/// \param columns Column names to select from manifest entries.
118121
ManifestGroup& Select(std::vector<std::string> columns);
119122

@@ -140,6 +143,9 @@ class ICEBERG_EXPORT ManifestGroup : public ErrorCollector {
140143
/// \brief Plan scan tasks for all matching data files.
141144
///
142145
/// Consumes this group and collects PlanFilesStream() into a vector.
146+
/// Callers must use std::move(group).PlanFiles(), or std::move(*group).PlanFiles()
147+
/// for an owning pointer, instead of calling on an lvalue. Do not reuse the consumed
148+
/// group; construct a new group to plan again.
143149
Result<std::vector<std::shared_ptr<FileScanTask>>> PlanFiles() &&;
144150

145151
/// \brief Lazily plan scan tasks for matching data files.
@@ -154,7 +160,9 @@ class ICEBERG_EXPORT ManifestGroup : public ErrorCollector {
154160
/// manifest at a time. Delete manifests are still read eagerly when creating the
155161
/// stream because delete files must be indexed before data-file planning can begin.
156162
/// Creating the stream consumes this group's configuration, so this method may only
157-
/// be called on an rvalue.
163+
/// be called on an rvalue. Use std::move(group).PlanFilesStream(), or
164+
/// std::move(*group).PlanFilesStream() for an owning pointer, and do not reuse the
165+
/// group.
158166
Result<FileScanTaskStreamPtr> PlanFilesStream() &&;
159167

160168
/// \brief Get all matching manifest entries.

‎src/iceberg/manifest/manifest_reader.h‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ using ManifestEntryStream = Stream<ManifestEntry>;
4545
using ManifestEntryStreamPtr = std::unique_ptr<ManifestEntryStream>;
4646

4747
/// \brief Read manifest entries from a manifest file.
48+
///
49+
/// Implementations must override EntriesStream() and LiveEntriesStream(), returning
50+
/// self-contained streams that may outlive the reader. These are the extension points;
51+
/// Entries() and LiveEntries() are non-virtual eager collection helpers. Custom readers
52+
/// that previously overrode the eager methods must migrate to the stream methods;
53+
/// no eager-to-stream fallback is provided.
4854
class ICEBERG_EXPORT ManifestReader {
4955
public:
5056
virtual ~ManifestReader() = default;

‎src/iceberg/test/manifest_group_test.cc‎

Lines changed: 70 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -369,18 +369,77 @@ TEST_P(ManifestGroupTest, PlanFilesDropsUnselectedStatsWithEqualityDeletes) {
369369
/*sequence_number=*/2, std::move(equality_delete))},
370370
partitioned_spec_);
371371

372-
ICEBERG_UNWRAP_OR_FAIL(
373-
auto group, ManifestGroup::Make(file_io_, schema_, GetSpecsById(), {data_manifest},
374-
{delete_manifest}));
375-
group->Select({"file_path"});
372+
for (bool use_executor : {false, true}) {
373+
SCOPED_TRACE(std::format("use_executor={}", use_executor));
374+
for (bool use_stream : {false, true}) {
375+
SCOPED_TRACE(std::format("use_stream={}", use_stream));
376+
test::ThreadExecutor executor;
377+
ICEBERG_UNWRAP_OR_FAIL(auto group,
378+
ManifestGroup::Make(file_io_, schema_, GetSpecsById(),
379+
{data_manifest}, {delete_manifest}));
380+
group->Select({"file_path"});
381+
if (use_executor) {
382+
group->PlanWith(std::ref(executor));
383+
}
384+
385+
std::vector<std::shared_ptr<FileScanTask>> tasks;
386+
if (use_stream) {
387+
ICEBERG_UNWRAP_OR_FAIL(auto stream, std::move(*group).PlanFilesStream());
388+
ICEBERG_UNWRAP_OR_FAIL(tasks, stream->ToVector());
389+
} else {
390+
ICEBERG_UNWRAP_OR_FAIL(tasks, std::move(*group).PlanFiles());
391+
}
392+
ASSERT_EQ(tasks.size(), 1);
393+
EXPECT_EQ(tasks.front()->data_file()->partition, part_value);
394+
ASSERT_EQ(tasks.front()->delete_files().size(), 1);
395+
EXPECT_EQ(tasks.front()->delete_files().front()->file_path,
396+
"/path/to/equality-delete.parquet");
397+
EXPECT_TRUE(tasks.front()->data_file()->lower_bounds.empty());
398+
EXPECT_TRUE(tasks.front()->data_file()->upper_bounds.empty());
399+
}
400+
}
401+
}
376402

377-
ICEBERG_UNWRAP_OR_FAIL(auto tasks, std::move(*group).PlanFiles());
378-
ASSERT_EQ(tasks.size(), 1);
379-
ASSERT_EQ(tasks.front()->delete_files().size(), 1);
380-
EXPECT_EQ(tasks.front()->delete_files().front()->file_path,
381-
"/path/to/equality-delete.parquet");
382-
EXPECT_TRUE(tasks.front()->data_file()->lower_bounds.empty());
383-
EXPECT_TRUE(tasks.front()->data_file()->upper_bounds.empty());
403+
TEST_P(ManifestGroupTest, PlanFilesStreamPreservesPartitionWithPositionDeletes) {
404+
auto version = GetParam();
405+
if (version < 2) {
406+
GTEST_SKIP() << "Position deletes only supported in V2+";
407+
}
408+
409+
constexpr int64_t kSnapshotId = 1000L;
410+
const auto part_value = PartitionValues({Literal::Int(0)});
411+
auto data_manifest = WriteDataManifest(
412+
version, kSnapshotId,
413+
{MakeEntry(ManifestStatus::kAdded, kSnapshotId, /*sequence_number=*/1,
414+
MakeDataFile("/path/to/data.parquet", part_value,
415+
partitioned_spec_->spec_id()))},
416+
partitioned_spec_);
417+
auto delete_manifest = WriteDeleteManifest(
418+
version, kSnapshotId,
419+
{MakeEntry(ManifestStatus::kAdded, kSnapshotId, /*sequence_number=*/2,
420+
MakePositionDeleteFile("/path/to/position-delete.parquet", part_value,
421+
partitioned_spec_->spec_id()))},
422+
partitioned_spec_);
423+
424+
for (bool use_executor : {false, true}) {
425+
SCOPED_TRACE(std::format("use_executor={}", use_executor));
426+
test::ThreadExecutor executor;
427+
ICEBERG_UNWRAP_OR_FAIL(auto group,
428+
ManifestGroup::Make(file_io_, schema_, GetSpecsById(),
429+
{data_manifest}, {delete_manifest}));
430+
group->Select({"file_path"});
431+
if (use_executor) {
432+
group->PlanWith(std::ref(executor));
433+
}
434+
435+
ICEBERG_UNWRAP_OR_FAIL(auto stream, std::move(*group).PlanFilesStream());
436+
ICEBERG_UNWRAP_OR_FAIL(auto tasks, stream->ToVector());
437+
ASSERT_EQ(tasks.size(), 1);
438+
EXPECT_EQ(tasks.front()->data_file()->partition, part_value);
439+
ASSERT_EQ(tasks.front()->delete_files().size(), 1);
440+
EXPECT_EQ(tasks.front()->delete_files().front()->file_path,
441+
"/path/to/position-delete.parquet");
442+
}
384443
}
385444

386445
TEST_P(ManifestGroupTest, IgnoreDeleted) {

‎src/iceberg/util/stream.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ namespace iceberg {
4040
/// Once Next() returns an error or std::nullopt, the stream is terminal. Subsequent
4141
/// calls return the same terminal result without invoking the implementation again.
4242
///
43+
/// This interface replaces Iterator<T>. Include iceberg/util/stream.h instead of
44+
/// iceberg/util/iterator.h and derive from Stream<T>, retaining the NextImpl() override.
45+
/// The former header and type alias are not provided.
46+
///
4347
/// \tparam T Value returned by the stream.
4448
template <typename T>
4549
class Stream {

0 commit comments

Comments
 (0)