Skip to content

Commit dcf92f5

Browse files
authored
feat: add fallible iterator utility (#905)
1 parent 02779a7 commit dcf92f5

6 files changed

Lines changed: 306 additions & 0 deletions

File tree

src/iceberg/test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ add_iceberg_test(util_test
135135
endian_test.cc
136136
file_io_test.cc
137137
formatter_test.cc
138+
iterator_test.cc
138139
lazy_test.cc
139140
location_util_test.cc
140141
math_util_internal_test.cc

src/iceberg/test/iterator_test.cc

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
#include "iceberg/util/iterator.h"
21+
22+
#include <memory>
23+
#include <optional>
24+
#include <type_traits>
25+
#include <vector>
26+
27+
#include <gtest/gtest.h>
28+
29+
#include "iceberg/test/matchers.h"
30+
31+
namespace iceberg {
32+
namespace {
33+
34+
class CopyOnly {
35+
public:
36+
explicit CopyOnly(int value) : value_(value) {}
37+
38+
CopyOnly(const CopyOnly&) = default;
39+
CopyOnly& operator=(const CopyOnly&) = default;
40+
CopyOnly(CopyOnly&&) = delete;
41+
CopyOnly& operator=(CopyOnly&&) = delete;
42+
43+
int value() const { return value_; }
44+
45+
private:
46+
int value_;
47+
};
48+
49+
static_assert(std::is_copy_constructible_v<CopyOnly>);
50+
static_assert(!std::is_move_constructible_v<CopyOnly>);
51+
52+
// Exercises ToVector() with values that can be copied but not moved.
53+
class CopyOnlyIterator final : public Iterator<CopyOnly> {
54+
private:
55+
Result<std::optional<CopyOnly>> NextImpl() override {
56+
if (next_ == 3) {
57+
return Result<std::optional<CopyOnly>>(std::in_place, std::nullopt);
58+
}
59+
return Result<std::optional<CopyOnly>>(std::in_place, std::in_place, next_++);
60+
}
61+
62+
int next_ = 0;
63+
};
64+
65+
// Exercises ToVector() with values that can be moved but not copied.
66+
class MoveOnlyIterator final : public Iterator<std::unique_ptr<int>> {
67+
public:
68+
int calls() const { return calls_; }
69+
70+
private:
71+
Result<std::optional<std::unique_ptr<int>>> NextImpl() override {
72+
++calls_;
73+
if (next_ == 3) {
74+
return Result<std::optional<std::unique_ptr<int>>>(std::in_place, std::nullopt);
75+
}
76+
return Result<std::optional<std::unique_ptr<int>>>(std::in_place, std::in_place,
77+
std::make_unique<int>(next_++));
78+
}
79+
80+
int next_ = 0;
81+
int calls_ = 0;
82+
};
83+
84+
// Exercises ToVector() error propagation after some values have been consumed.
85+
class FailingIterator final : public Iterator<int> {
86+
public:
87+
int calls() const { return calls_; }
88+
89+
private:
90+
Result<std::optional<int>> NextImpl() override {
91+
++calls_;
92+
if (next_ < 2) {
93+
return Result<std::optional<int>>(std::in_place, std::in_place, next_++);
94+
}
95+
return Invalid("iteration failed");
96+
}
97+
98+
int next_ = 0;
99+
int calls_ = 0;
100+
};
101+
102+
static_assert(std::is_move_constructible_v<CopyOnlyIterator>);
103+
static_assert(std::is_move_assignable_v<CopyOnlyIterator>);
104+
static_assert(std::is_move_constructible_v<MoveOnlyIterator>);
105+
static_assert(std::is_move_assignable_v<MoveOnlyIterator>);
106+
107+
TEST(IteratorTest, ToVectorSupportsCopyOnlyValues) {
108+
CopyOnlyIterator iterator;
109+
110+
ICEBERG_UNWRAP_OR_FAIL(auto values, iterator.ToVector());
111+
112+
ASSERT_EQ(values.size(), 3);
113+
EXPECT_EQ(values[0].value(), 0);
114+
EXPECT_EQ(values[1].value(), 1);
115+
EXPECT_EQ(values[2].value(), 2);
116+
}
117+
118+
TEST(IteratorTest, ToVectorSupportsMoveOnlyValues) {
119+
MoveOnlyIterator iterator;
120+
121+
ICEBERG_UNWRAP_OR_FAIL(auto values, iterator.ToVector());
122+
123+
ASSERT_EQ(values.size(), 3);
124+
EXPECT_EQ(*values[0], 0);
125+
EXPECT_EQ(*values[1], 1);
126+
EXPECT_EQ(*values[2], 2);
127+
}
128+
129+
TEST(IteratorTest, NextRemainsAtEndAfterExhaustion) {
130+
MoveOnlyIterator iterator;
131+
ICEBERG_UNWRAP_OR_FAIL(auto values, iterator.ToVector());
132+
ASSERT_EQ(values.size(), 3);
133+
EXPECT_EQ(iterator.calls(), 4);
134+
135+
for (int i = 0; i < 2; ++i) {
136+
auto result = iterator.Next();
137+
ASSERT_TRUE(result.has_value());
138+
EXPECT_FALSE(result->has_value());
139+
}
140+
EXPECT_EQ(iterator.calls(), 4);
141+
}
142+
143+
TEST(IteratorTest, ToVectorPropagatesErrorsAfterPartialConsumption) {
144+
FailingIterator iterator;
145+
146+
ICEBERG_UNWRAP_OR_FAIL(auto first, iterator.Next());
147+
ASSERT_TRUE(first.has_value());
148+
EXPECT_EQ(first.value(), 0);
149+
150+
auto result = iterator.ToVector();
151+
152+
EXPECT_THAT(result, IsError(ErrorKind::kInvalid));
153+
EXPECT_THAT(result, HasErrorMessage("iteration failed"));
154+
}
155+
156+
TEST(IteratorTest, NextRepeatsErrorWithoutAdvancing) {
157+
FailingIterator iterator;
158+
auto first_error = iterator.ToVector();
159+
EXPECT_THAT(first_error, IsError(ErrorKind::kInvalid));
160+
EXPECT_THAT(first_error, HasErrorMessage("iteration failed"));
161+
EXPECT_EQ(iterator.calls(), 3);
162+
163+
for (int i = 0; i < 2; ++i) {
164+
auto result = iterator.Next();
165+
EXPECT_THAT(result, IsError(ErrorKind::kInvalid));
166+
EXPECT_THAT(result, HasErrorMessage("iteration failed"));
167+
}
168+
EXPECT_EQ(iterator.calls(), 3);
169+
}
170+
171+
} // namespace
172+
} // namespace iceberg

src/iceberg/test/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ iceberg_tests = {
110110
'executor_util_test.cc',
111111
'file_io_test.cc',
112112
'formatter_test.cc',
113+
'iterator_test.cc',
113114
'lazy_test.cc',
114115
'location_util_test.cc',
115116
'math_util_internal_test.cc',

src/iceberg/type_fwd.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,8 @@ struct SessionContext;
229229

230230
/// \brief Task execution.
231231
class Executor;
232+
template <typename T>
233+
class Iterator;
232234

233235
/// \brief Metrics reporting.
234236
class MetricsReporter;

src/iceberg/util/iterator.h

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
#pragma once
21+
22+
/// \file iceberg/util/iterator.h
23+
/// \brief Pull-based iterator interface for fallible, lazily produced values.
24+
25+
#include <deque>
26+
#include <optional>
27+
#include <type_traits>
28+
#include <utility>
29+
#include <vector>
30+
31+
#include "iceberg/result.h"
32+
33+
namespace iceberg {
34+
35+
/// \brief A pull-based iterator whose reads may fail.
36+
///
37+
/// Iterator implementations own any resources needed to produce values. Destroying an
38+
/// iterator releases those resources, including when iteration stops before reaching the
39+
/// end. Iterators are not thread-safe unless an implementation explicitly says otherwise.
40+
/// Once Next() returns an error or std::nullopt, the iterator is terminal. Subsequent
41+
/// calls return the same terminal result without invoking the implementation again.
42+
///
43+
/// \tparam T Value returned by the iterator.
44+
template <typename T>
45+
class Iterator {
46+
public:
47+
virtual ~Iterator() = default;
48+
49+
Iterator() = default;
50+
Iterator(const Iterator&) = delete;
51+
Iterator& operator=(const Iterator&) = delete;
52+
Iterator(Iterator&&) noexcept = default;
53+
Iterator& operator=(Iterator&&) noexcept = default;
54+
55+
/// \brief Return the next value, or std::nullopt when the iterator is exhausted.
56+
///
57+
/// After this method returns an error or std::nullopt, subsequent calls return the same
58+
/// terminal result without invoking NextImpl().
59+
Result<std::optional<T>> Next() {
60+
if (error_.has_value()) {
61+
return std::unexpected(*error_);
62+
}
63+
if (finished_) {
64+
return std::nullopt;
65+
}
66+
67+
auto result = NextImpl();
68+
if (!result.has_value()) {
69+
error_ = result.error();
70+
} else if (!result.value().has_value()) {
71+
finished_ = true;
72+
}
73+
return result;
74+
}
75+
76+
/// \brief Consume the remaining values into a vector.
77+
Result<std::vector<T>> ToVector() {
78+
auto collect = [this](auto& values, auto append,
79+
auto finish) -> Result<std::vector<T>> {
80+
while (true) {
81+
auto result = Next();
82+
if (!result.has_value()) {
83+
return std::unexpected(std::move(result.error()));
84+
}
85+
auto& value = result.value();
86+
if (!value.has_value()) {
87+
return finish(values);
88+
}
89+
append(values, value.value());
90+
}
91+
};
92+
93+
if constexpr (!std::is_move_constructible_v<T>) {
94+
static_assert(std::is_copy_constructible_v<T>,
95+
"Iterator::ToVector requires T to be move- or copy-constructible");
96+
97+
// For strictly copy-only T, collecting directly into a vector can repeatedly copy
98+
// previously collected elements during vector growth. Stage values in a deque,
99+
// then copy once into an exactly sized vector.
100+
std::deque<T> values;
101+
return collect(
102+
values, [](auto& destination, const T& value) { destination.push_back(value); },
103+
[](const auto& source) {
104+
return std::vector<T>(source.cbegin(), source.cend());
105+
});
106+
} else {
107+
std::vector<T> values;
108+
return collect(
109+
values,
110+
[](auto& destination, T& value) {
111+
destination.push_back(std::move_if_noexcept(value));
112+
},
113+
[](auto& source) { return std::move(source); });
114+
}
115+
}
116+
117+
protected:
118+
/// \brief Produce the next value for Next().
119+
///
120+
/// Implementations must return std::nullopt when exhausted. Next() makes the
121+
/// terminal state sticky, so implementations are not called after exhaustion or error.
122+
virtual Result<std::optional<T>> NextImpl() = 0;
123+
124+
private:
125+
bool finished_ = false;
126+
std::optional<Error> error_;
127+
};
128+
129+
} // namespace iceberg

src/iceberg/util/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ install_headers(
3232
'formatter.h',
3333
'functional.h',
3434
'int128.h',
35+
'iterator.h',
3536
'lazy.h',
3637
'location_util.h',
3738
'macros.h',

0 commit comments

Comments
 (0)