Skip to content

Commit 1ba9c7d

Browse files
committed
IsBoundVisitor mixed bound/unbound predicate should error
1 parent d07b756 commit 1ba9c7d

File tree

3 files changed

+67
-31
lines changed

3 files changed

+67
-31
lines changed

src/iceberg/expression/binder.cc

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,25 @@
1919

2020
#include "iceberg/expression/binder.h"
2121

22+
#include "iceberg/util/macros.h"
23+
2224
namespace iceberg {
2325

26+
namespace {
27+
28+
Result<std::optional<bool>> CombineResults(const std::optional<bool>& is_left_bound,
29+
const std::optional<bool>& is_right_bound) {
30+
if (is_left_bound.has_value()) {
31+
ICEBERG_CHECK(!is_right_bound.has_value() || is_left_bound == is_right_bound,
32+
"Found partially bound expression");
33+
return is_left_bound;
34+
} else {
35+
return is_right_bound;
36+
}
37+
}
38+
39+
} // anonymous namespace
40+
2441
Binder::Binder(const Schema& schema, bool case_sensitive)
2542
: schema_(schema), case_sensitive_(case_sensitive) {}
2643

@@ -79,36 +96,47 @@ Result<std::shared_ptr<Expression>> Binder::Aggregate(
7996
Result<bool> IsBoundVisitor::IsBound(const std::shared_ptr<Expression>& expr) {
8097
ICEBERG_DCHECK(expr != nullptr, "Expression cannot be null");
8198
IsBoundVisitor visitor;
82-
return Visit<bool, IsBoundVisitor>(expr, visitor);
99+
auto visit_result = Visit<std::optional<bool>, IsBoundVisitor>(expr, visitor);
100+
ICEBERG_RETURN_UNEXPECTED(visit_result);
101+
auto result = std::move(visit_result.value());
102+
// If the result is null, return true
103+
return result.value_or(true);
83104
}
84105

85-
Result<bool> IsBoundVisitor::AlwaysTrue() { return true; }
106+
Result<std::optional<bool>> IsBoundVisitor::AlwaysTrue() { return std::nullopt; }
86107

87-
Result<bool> IsBoundVisitor::AlwaysFalse() { return true; }
108+
Result<std::optional<bool>> IsBoundVisitor::AlwaysFalse() { return std::nullopt; }
88109

89-
Result<bool> IsBoundVisitor::Not(bool child_result) { return child_result; }
110+
Result<std::optional<bool>> IsBoundVisitor::Not(const std::optional<bool>& child_result) {
111+
return child_result;
112+
}
90113

91-
Result<bool> IsBoundVisitor::And(bool left_result, bool right_result) {
92-
return left_result && right_result;
114+
Result<std::optional<bool>> IsBoundVisitor::And(const std::optional<bool>& left_result,
115+
const std::optional<bool>& right_result) {
116+
return CombineResults(left_result, right_result);
93117
}
94118

95-
Result<bool> IsBoundVisitor::Or(bool left_result, bool right_result) {
96-
return left_result && right_result;
119+
Result<std::optional<bool>> IsBoundVisitor::Or(const std::optional<bool>& left_result,
120+
const std::optional<bool>& right_result) {
121+
return CombineResults(left_result, right_result);
97122
}
98123

99-
Result<bool> IsBoundVisitor::Predicate(const std::shared_ptr<BoundPredicate>& pred) {
124+
Result<std::optional<bool>> IsBoundVisitor::Predicate(
125+
const std::shared_ptr<BoundPredicate>& pred) {
100126
return true;
101127
}
102128

103-
Result<bool> IsBoundVisitor::Predicate(const std::shared_ptr<UnboundPredicate>& pred) {
129+
Result<std::optional<bool>> IsBoundVisitor::Predicate(
130+
const std::shared_ptr<UnboundPredicate>& pred) {
104131
return false;
105132
}
106133

107-
Result<bool> IsBoundVisitor::Aggregate(const std::shared_ptr<BoundAggregate>& aggregate) {
134+
Result<std::optional<bool>> IsBoundVisitor::Aggregate(
135+
const std::shared_ptr<BoundAggregate>& aggregate) {
108136
return true;
109137
}
110138

111-
Result<bool> IsBoundVisitor::Aggregate(
139+
Result<std::optional<bool>> IsBoundVisitor::Aggregate(
112140
const std::shared_ptr<UnboundAggregate>& aggregate) {
113141
return false;
114142
}

src/iceberg/expression/binder.h

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
/// \file iceberg/expression/binder.h
2323
/// Bind an expression to a schema.
2424

25+
#include <optional>
26+
2527
#include "iceberg/expression/expression_visitor.h"
2628

2729
namespace iceberg {
@@ -58,19 +60,25 @@ class ICEBERG_EXPORT Binder : public ExpressionVisitor<std::shared_ptr<Expressio
5860
const bool case_sensitive_;
5961
};
6062

61-
class ICEBERG_EXPORT IsBoundVisitor : public ExpressionVisitor<bool> {
63+
class ICEBERG_EXPORT IsBoundVisitor : public ExpressionVisitor<std::optional<bool>> {
6264
public:
6365
static Result<bool> IsBound(const std::shared_ptr<Expression>& expr);
6466

65-
Result<bool> AlwaysTrue() override;
66-
Result<bool> AlwaysFalse() override;
67-
Result<bool> Not(bool child_result) override;
68-
Result<bool> And(bool left_result, bool right_result) override;
69-
Result<bool> Or(bool left_result, bool right_result) override;
70-
Result<bool> Predicate(const std::shared_ptr<BoundPredicate>& pred) override;
71-
Result<bool> Predicate(const std::shared_ptr<UnboundPredicate>& pred) override;
72-
Result<bool> Aggregate(const std::shared_ptr<BoundAggregate>& aggregate) override;
73-
Result<bool> Aggregate(const std::shared_ptr<UnboundAggregate>& aggregate) override;
67+
Result<std::optional<bool>> AlwaysTrue() override;
68+
Result<std::optional<bool>> AlwaysFalse() override;
69+
Result<std::optional<bool>> Not(const std::optional<bool>& child_result) override;
70+
Result<std::optional<bool>> And(const std::optional<bool>& left_result,
71+
const std::optional<bool>& right_result) override;
72+
Result<std::optional<bool>> Or(const std::optional<bool>& left_result,
73+
const std::optional<bool>& right_result) override;
74+
Result<std::optional<bool>> Predicate(
75+
const std::shared_ptr<BoundPredicate>& pred) override;
76+
Result<std::optional<bool>> Predicate(
77+
const std::shared_ptr<UnboundPredicate>& pred) override;
78+
Result<std::optional<bool>> Aggregate(
79+
const std::shared_ptr<BoundAggregate>& aggregate) override;
80+
Result<std::optional<bool>> Aggregate(
81+
const std::shared_ptr<UnboundAggregate>& aggregate) override;
7482
};
7583

7684
// TODO(gangwu): add the Java parity `ReferenceVisitor`

src/iceberg/test/expression_visitor_test.cc

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -332,14 +332,14 @@ TEST_F(IsBoundVisitorTest, AndWithBoundChildren) {
332332
}
333333

334334
TEST_F(IsBoundVisitorTest, AndWithUnboundChild) {
335-
// AND with any unbound child should return false
335+
// AND with mixed bound/unbound children should error
336336
auto bound_pred = Expressions::Equal("name", Literal::String("Alice"));
337337
ICEBERG_UNWRAP_OR_FAIL(auto pred1, Bind(bound_pred));
338338
auto pred2 = Expressions::Equal("age", Literal::Int(25)); // unbound
339339
auto mixed_and = Expressions::And(pred1, pred2);
340340

341-
ICEBERG_UNWRAP_OR_FAIL(auto is_bound, IsBoundVisitor::IsBound(mixed_and));
342-
EXPECT_FALSE(is_bound);
341+
auto result = IsBoundVisitor::IsBound(mixed_and);
342+
EXPECT_THAT(result, HasErrorMessage("Found partially bound expression"));
343343
}
344344

345345
TEST_F(IsBoundVisitorTest, OrWithBoundChildren) {
@@ -354,14 +354,14 @@ TEST_F(IsBoundVisitorTest, OrWithBoundChildren) {
354354
}
355355

356356
TEST_F(IsBoundVisitorTest, OrWithUnboundChild) {
357-
// OR with any unbound child should return false
357+
// OR with mixed bound/unbound children should error
358358
auto pred1 = Expressions::IsNull("name"); // unbound
359359
auto bound_pred2 = Expressions::Equal("age", Literal::Int(25));
360360
ICEBERG_UNWRAP_OR_FAIL(auto pred2, Bind(bound_pred2));
361361
auto mixed_or = Expressions::Or(pred1, pred2);
362362

363-
ICEBERG_UNWRAP_OR_FAIL(auto is_bound, IsBoundVisitor::IsBound(mixed_or));
364-
EXPECT_FALSE(is_bound);
363+
auto result = IsBoundVisitor::IsBound(mixed_or);
364+
EXPECT_THAT(result, HasErrorMessage("Found partially bound expression"));
365365
}
366366

367367
TEST_F(IsBoundVisitorTest, NotWithBoundChild) {
@@ -395,15 +395,15 @@ TEST_F(IsBoundVisitorTest, ComplexExpression) {
395395
ICEBERG_UNWRAP_OR_FAIL(auto is_bound, IsBoundVisitor::IsBound(bound_complex));
396396
EXPECT_TRUE(is_bound);
397397

398-
// Complex expression: one unbound should return false
398+
// Complex expression: mixed bound/unbound children should error
399399
auto unbound_pred = Expressions::Equal("name", Literal::String("Alice"));
400400
ICEBERG_UNWRAP_OR_FAIL(auto bound_pred2, Bind(pred2));
401401
ICEBERG_UNWRAP_OR_FAIL(auto bound_pred3, Bind(pred3));
402402
auto mixed_and = Expressions::And(unbound_pred, bound_pred2);
403403
auto mixed_complex = Expressions::Or(mixed_and, bound_pred3);
404404

405-
ICEBERG_UNWRAP_OR_FAIL(auto is_bound_mixed, IsBoundVisitor::IsBound(mixed_complex));
406-
EXPECT_FALSE(is_bound_mixed);
405+
auto result_mixed = IsBoundVisitor::IsBound(mixed_complex);
406+
EXPECT_THAT(result_mixed, HasErrorMessage("Found partially bound expression"));
407407
}
408408

409409
class RewriteNotTest : public ExpressionVisitorTest {};

0 commit comments

Comments
 (0)