Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/iceberg/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ set(ICEBERG_SOURCES
transform.cc
transform_function.cc
type.cc
snapshot.cc)
snapshot.cc
expression.cc)

set(ICEBERG_STATIC_BUILD_INTERFACE_LIBS)
set(ICEBERG_SHARED_BUILD_INTERFACE_LIBS)
Expand Down
65 changes: 65 additions & 0 deletions src/iceberg/expression.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* 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 "expression.h"

#include <format>

namespace iceberg {

// True implementation
const std::shared_ptr<True>& True::Instance() {
static const std::shared_ptr<True> instance{new True()};
return instance;
}

Result<std::shared_ptr<Expression>> True::Negate() const { return False::Instance(); }

// False implementation
const std::shared_ptr<False>& False::Instance() {
static const std::shared_ptr<False> instance = std::shared_ptr<False>(new False());
return instance;
}

Result<std::shared_ptr<Expression>> False::Negate() const { return True::Instance(); }

// And implementation
And::And(std::shared_ptr<Expression> left, std::shared_ptr<Expression> right)
: left_(std::move(left)), right_(std::move(right)) {}

std::string And::ToString() const {
return std::format("({} and {})", left_->ToString(), right_->ToString());
}

Result<std::shared_ptr<Expression>> And::Negate() const {
// TODO(yingcai-cy): Implement Or expression
return unexpected(
Error(ErrorKind::kInvalidExpression, "And negation not yet implemented"));
}

bool And::Equals(const Expression& expr) const {
if (expr.op() == Operation::kAnd) {
const auto& other = static_cast<const And&>(expr);
return (left_->Equals(*other.left()) && right_->Equals(*other.right())) ||
(left_->Equals(*other.right()) && right_->Equals(*other.left()));
Comment on lines +58 to +60
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Java we call this isEquivalentTo: https://github.com/apache/iceberg/blob/97d1107d60b42d29306c663f23f869fc1d439e6b/api/src/main/java/org/apache/iceberg/expressions/Or.java#L43-L52, which is different from being equal. Should we follow the same naming for clarity?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good suggestion! In the first version, it was isEquivalentTo. I followed the style of other classes in the codebase and switched to Equals for consistency.

}
return false;
}

} // namespace iceberg
163 changes: 163 additions & 0 deletions src/iceberg/expression.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/*
* 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/expression.h
/// Expression interface for Iceberg table operations.

#include <memory>
#include <string>

#include "iceberg/expected.h"
#include "iceberg/iceberg_export.h"
#include "iceberg/result.h"

namespace iceberg {

/// \brief Represents a boolean expression tree.
class ICEBERG_EXPORT Expression {
public:
/// Operation types for expressions
enum class Operation {
kTrue,
kFalse,
kIsNull,
kNotNull,
kIsNan,
kNotNan,
kLt,
kLtEq,
kGt,
kGtEq,
kEq,
kNotEq,
kIn,
kNotIn,
kNot,
kAnd,
kOr,
kStartsWith,
kNotStartsWith,
kCount,
kCountStar,
kMax,
kMin
};

virtual ~Expression() = default;

/// \brief Returns the operation for an expression node.
virtual Operation op() const = 0;

/// \brief Returns the negation of this expression, equivalent to not(this).
virtual Result<std::shared_ptr<Expression>> Negate() const {
return unexpected(
Error(ErrorKind::kInvalidExpression, "Expression cannot be negated"));
}

/// \brief Returns whether this expression will accept the same values as another.
/// \param other another expression
/// \return true if the expressions are equivalent
virtual bool Equals(const Expression& other) const {
// only bound predicates can be equivalent
return false;
}

virtual std::string ToString() const { return "Expression"; }
};

/// \brief An Expression that is always true.
///
/// Represents a boolean predicate that always evaluates to true.
class ICEBERG_EXPORT True : public Expression {
public:
/// \brief Returns the singleton instance
static const std::shared_ptr<True>& Instance();

Operation op() const override { return Operation::kTrue; }

std::string ToString() const override { return "true"; }

Result<std::shared_ptr<Expression>> Negate() const override;

bool Equals(const Expression& other) const override {
return other.op() == Operation::kTrue;
}

private:
constexpr True() = default;
};

/// \brief An expression that is always false.
class ICEBERG_EXPORT False : public Expression {
public:
/// \brief Returns the singleton instance
static const std::shared_ptr<False>& Instance();

Operation op() const override { return Operation::kFalse; }

std::string ToString() const override { return "false"; }

Result<std::shared_ptr<Expression>> Negate() const override;

bool Equals(const Expression& other) const override {
return other.op() == Operation::kFalse;
}

private:
constexpr False() = default;
};

/// \brief An Expression that represents a logical AND operation between two expressions.
///
/// This expression evaluates to true if and only if both of its child expressions
/// evaluate to true.
class ICEBERG_EXPORT And : public Expression {
public:
/// \brief Constructs an And expression from two sub-expressions.
///
/// \param left The left operand of the AND expression
/// \param right The right operand of the AND expression
And(std::shared_ptr<Expression> left, std::shared_ptr<Expression> right);

/// \brief Returns the left operand of the AND expression.
///
/// \return The left operand of the AND expression
const std::shared_ptr<Expression>& left() const { return left_; }

/// \brief Returns the right operand of the AND expression.
///
/// \return The right operand of the AND expression
const std::shared_ptr<Expression>& right() const { return right_; }

Operation op() const override { return Operation::kAnd; }

std::string ToString() const override;

Result<std::shared_ptr<Expression>> Negate() const override;

bool Equals(const Expression& other) const override;

private:
std::shared_ptr<Expression> left_;
std::shared_ptr<Expression> right_;
};

} // namespace iceberg
1 change: 1 addition & 0 deletions src/iceberg/result.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ enum class ErrorKind {
kNotImplemented,
kUnknownError,
kNotSupported,
kInvalidExpression,
kJsonParseError,
};

Expand Down
5 changes: 5 additions & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ target_sources(expected_test PRIVATE expected_test.cc)
target_link_libraries(expected_test PRIVATE iceberg_static GTest::gtest_main GTest::gmock)
add_test(NAME expected_test COMMAND expected_test)

add_executable(expression_test)
target_sources(expression_test PRIVATE expression_test.cc)
target_link_libraries(expression_test PRIVATE iceberg_static GTest::gtest_main GTest::gmock)
add_test(NAME expression_test COMMAND expression_test)

if(ICEBERG_BUILD_BUNDLE)
add_executable(avro_test)
target_sources(avro_test PRIVATE avro_test.cc)
Expand Down
67 changes: 67 additions & 0 deletions test/expression_test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* 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/expression.h"

#include <memory>

#include <gtest/gtest.h>

namespace iceberg {

TEST(TrueFalseTest, Basic) {
// Test negation of False returns True
auto false_instance = False::Instance();
auto negated = false_instance->Negate();

EXPECT_TRUE(negated.has_value());

// Check that negated expression is True
auto true_expr = negated.value();
EXPECT_EQ(true_expr->op(), Expression::Operation::kTrue);

EXPECT_EQ(true_expr->ToString(), "true");

// Test negation of True returns false
auto true_instance = True::Instance();
negated = true_instance->Negate();

EXPECT_TRUE(negated.has_value());

// Check that negated expression is False
auto false_expr = negated.value();
EXPECT_EQ(false_expr->op(), Expression::Operation::kFalse);

EXPECT_EQ(false_expr->ToString(), "false");
}

TEST(ANDTest, Basic) {
// Create two True expressions
auto true_expr1 = True::Instance();
auto true_expr2 = True::Instance();

// Create an AND expression
auto and_expr = std::make_shared<And>(true_expr1, true_expr2);

EXPECT_EQ(and_expr->op(), Expression::Operation::kAnd);
EXPECT_EQ(and_expr->ToString(), "(true and true)");
EXPECT_EQ(and_expr->left()->op(), Expression::Operation::kTrue);
EXPECT_EQ(and_expr->right()->op(), Expression::Operation::kTrue);
}
} // namespace iceberg
Loading