You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@iceberg.apache.org by "SuKi2cn (via GitHub)" <gi...@apache.org> on 2025/11/21 00:32:32 UTC

[PR] feat: add aggregate expressions and evaluator [iceberg-cpp]

SuKi2cn opened a new pull request, #335:
URL: https://github.com/apache/iceberg-cpp/pull/335

   This PR addresses issue #330 by introducing aggregate expressions & execution support.
   
   * Add aggregate expression family (count / count_null / count_star / max / min)
     with bound/unbound types, visitor and binder support.
   
   * Add `AggregateEvaluator` for count/max/min execution over `StructLike` rows.
   
   * Expose aggregate factories in `Expressions` and wire into CMake/Meson builds
     with new aggregate tests.
   
   #### Tests
   
   * `cmake --build build-clang`
   * `ctest --test-dir build-clang --output-on-failure`


-- 
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: issues-unsubscribe@iceberg.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org


Re: [PR] feat: add aggregate expressions and evaluator [iceberg-cpp]

Posted by "Jinchul81 (via GitHub)" <gi...@apache.org>.
Jinchul81 commented on code in PR #335:
URL: https://github.com/apache/iceberg-cpp/pull/335#discussion_r2553970555


##########
src/iceberg/expression/aggregate.cc:
##########
@@ -0,0 +1,289 @@
+/*
+ * 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/aggregate.h"
+
+#include <format>
+#include <optional>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/expression/expression.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/type.h"
+#include "iceberg/util/checked_cast.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+std::string OperationToPrefix(Expression::Operation op) {
+  switch (op) {
+    case Expression::Operation::kMax:
+      return "max";
+    case Expression::Operation::kMin:
+      return "min";
+    case Expression::Operation::kCount:
+    case Expression::Operation::kCountStar:
+      return "count";
+    default:
+      break;
+  }
+  return "aggregate";
+}
+
+Result<std::shared_ptr<PrimitiveType>> GetPrimitiveType(const BoundTerm& term) {
+  auto primitive = std::dynamic_pointer_cast<PrimitiveType>(term.type());
+  if (primitive == nullptr) {
+    return InvalidExpression("Aggregate requires primitive type, got {}",
+                             term.type()->ToString());
+  }
+  return primitive;
+}
+
+}  // namespace
+
+CountAggregate::CountAggregate(Expression::Operation op, Mode mode,
+                               std::shared_ptr<UnboundTerm<BoundReference>> term,
+                               std::shared_ptr<NamedReference> reference)
+    : UnboundAggregate(op),
+      mode_(mode),
+      term_(std::move(term)),
+      reference_(std::move(reference)) {}
+
+Result<std::unique_ptr<CountAggregate>> CountAggregate::Count(
+    std::shared_ptr<UnboundTerm<BoundReference>> term) {
+  auto ref = term->reference();
+  return std::unique_ptr<CountAggregate>(new CountAggregate(
+      Expression::Operation::kCount, Mode::kNonNull, std::move(term), std::move(ref)));
+}
+
+Result<std::unique_ptr<CountAggregate>> CountAggregate::CountNull(
+    std::shared_ptr<UnboundTerm<BoundReference>> term) {
+  auto ref = term->reference();
+  return std::unique_ptr<CountAggregate>(new CountAggregate(
+      Expression::Operation::kCount, Mode::kNull, std::move(term), std::move(ref)));
+}
+
+std::unique_ptr<CountAggregate> CountAggregate::CountStar() {
+  return std::unique_ptr<CountAggregate>(new CountAggregate(
+      Expression::Operation::kCountStar, Mode::kStar, nullptr, nullptr));
+}
+
+std::string CountAggregate::ToString() const {
+  if (mode_ == Mode::kStar) {

Review Comment:
   I am still curious the special handling should be required in not only C++ but also Java. Can you please try to apply `kStar` in the generic manner? I don't accept we just follow the implementation of the Java version.



-- 
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: issues-unsubscribe@iceberg.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org


Re: [PR] feat: add aggregate expressions and evaluator [iceberg-cpp]

Posted by "SuKi2cn (via GitHub)" <gi...@apache.org>.
SuKi2cn commented on code in PR #335:
URL: https://github.com/apache/iceberg-cpp/pull/335#discussion_r2553853157


##########
src/iceberg/expression/aggregate.cc:
##########
@@ -0,0 +1,289 @@
+/*
+ * 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/aggregate.h"
+
+#include <format>
+#include <optional>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/expression/expression.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/type.h"
+#include "iceberg/util/checked_cast.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+std::string OperationToPrefix(Expression::Operation op) {
+  switch (op) {
+    case Expression::Operation::kMax:
+      return "max";
+    case Expression::Operation::kMin:
+      return "min";
+    case Expression::Operation::kCount:
+    case Expression::Operation::kCountStar:
+      return "count";
+    default:
+      break;
+  }
+  return "aggregate";
+}
+
+Result<std::shared_ptr<PrimitiveType>> GetPrimitiveType(const BoundTerm& term) {
+  auto primitive = std::dynamic_pointer_cast<PrimitiveType>(term.type());
+  if (primitive == nullptr) {
+    return InvalidExpression("Aggregate requires primitive type, got {}",
+                             term.type()->ToString());
+  }
+  return primitive;
+}
+
+}  // namespace
+
+CountAggregate::CountAggregate(Expression::Operation op, Mode mode,
+                               std::shared_ptr<UnboundTerm<BoundReference>> term,
+                               std::shared_ptr<NamedReference> reference)
+    : UnboundAggregate(op),
+      mode_(mode),
+      term_(std::move(term)),
+      reference_(std::move(reference)) {}
+
+Result<std::unique_ptr<CountAggregate>> CountAggregate::Count(
+    std::shared_ptr<UnboundTerm<BoundReference>> term) {
+  auto ref = term->reference();
+  return std::unique_ptr<CountAggregate>(new CountAggregate(
+      Expression::Operation::kCount, Mode::kNonNull, std::move(term), std::move(ref)));
+}
+
+Result<std::unique_ptr<CountAggregate>> CountAggregate::CountNull(
+    std::shared_ptr<UnboundTerm<BoundReference>> term) {
+  auto ref = term->reference();
+  return std::unique_ptr<CountAggregate>(new CountAggregate(
+      Expression::Operation::kCount, Mode::kNull, std::move(term), std::move(ref)));
+}
+
+std::unique_ptr<CountAggregate> CountAggregate::CountStar() {
+  return std::unique_ptr<CountAggregate>(new CountAggregate(
+      Expression::Operation::kCountStar, Mode::kStar, nullptr, nullptr));
+}
+
+std::string CountAggregate::ToString() const {
+  if (mode_ == Mode::kStar) {

Review Comment:
   > Why did you special handling for `kStar`?
   
   The special handling for `kStar` is intentional and mirrors the Java implementation.
   
   In the Java version, `Aggregate.toString()` has a dedicated branch for `Operation.COUNT_STAR` that renders: `count(*)`
   
   In the C++ design, the different COUNT variants are represented via `BoundCountAggregate::Mode`
   (`kNonNull`, `kNull`, `kStar`). Therefore, this `kStar` special case is the equivalent of the Java
   `COUNT_STAR` branch, ensuring we correctly print `count(*)` for `COUNT(*)` semantics.
   
   In other words:
   - `Mode::kStar`   → `COUNT(*)`
   - `Mode::kNonNull` → `COUNT(column)`
   - `Mode::kNull` → `COUNT_NULL(column)`
   
   This preserves consistency with the Java behavior while adapting to the C++ expression model.
   



-- 
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: issues-unsubscribe@iceberg.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org


Re: [PR] feat: add aggregate expressions and evaluator [iceberg-cpp]

Posted by "wgtmac (via GitHub)" <gi...@apache.org>.
wgtmac commented on code in PR #335:
URL: https://github.com/apache/iceberg-cpp/pull/335#discussion_r2549038441


##########
src/iceberg/expression/aggregate.h:
##########
@@ -0,0 +1,186 @@
+/*
+ * 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/aggregate.h
+/// Aggregate expression definitions.
+
+#include <memory>
+
+#include "iceberg/expression/expression.h"
+#include "iceberg/expression/term.h"
+#include "iceberg/result.h"
+
+namespace iceberg {
+
+class AggregateEvaluator;
+
+/// \brief Base class for aggregate expressions.
+class ICEBERG_EXPORT Aggregate : public Expression {
+ public:
+  ~Aggregate() override = default;
+
+  Expression::Operation op() const override { return operation_; }
+
+  bool is_unbound_aggregate() const override { return false; }
+  bool is_bound_aggregate() const override { return false; }
+
+ protected:
+  explicit Aggregate(Expression::Operation op) : operation_(op) {}
+
+ private:
+  Expression::Operation operation_;
+};
+
+/// \brief Unbound aggregate with an optional term.
+class ICEBERG_EXPORT UnboundAggregate : public Aggregate, public Unbound<Expression> {
+ public:
+  ~UnboundAggregate() override = default;
+
+  bool is_unbound_aggregate() const override { return true; }
+
+  /// \brief Returns the unbound reference if the aggregate has a term.
+  std::shared_ptr<NamedReference> reference() override = 0;
+
+ protected:
+  explicit UnboundAggregate(Expression::Operation op) : Aggregate(op) {}
+};
+
+/// \brief Bound aggregate with an optional term.
+class ICEBERG_EXPORT BoundAggregate : public Aggregate {
+ public:
+  ~BoundAggregate() override = default;
+
+  bool is_bound_aggregate() const override { return true; }
+
+  const std::shared_ptr<BoundTerm>& term() const { return term_; }
+
+ protected:
+  BoundAggregate(Expression::Operation op, std::shared_ptr<BoundTerm> term)
+      : Aggregate(op), term_(std::move(term)) {}
+
+ private:
+  std::shared_ptr<BoundTerm> term_;
+};
+
+/// \brief COUNT aggregate variants.
+class ICEBERG_EXPORT CountAggregate : public UnboundAggregate {

Review Comment:
   We don't need to define subclass for unbound aggregates. Please check the current design of `Predicate` in both C++ and Java implementations.



##########
src/iceberg/expression/aggregate.h:
##########
@@ -0,0 +1,186 @@
+/*
+ * 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/aggregate.h
+/// Aggregate expression definitions.
+
+#include <memory>
+
+#include "iceberg/expression/expression.h"
+#include "iceberg/expression/term.h"
+#include "iceberg/result.h"
+
+namespace iceberg {
+
+class AggregateEvaluator;
+
+/// \brief Base class for aggregate expressions.
+class ICEBERG_EXPORT Aggregate : public Expression {
+ public:
+  ~Aggregate() override = default;
+
+  Expression::Operation op() const override { return operation_; }
+
+  bool is_unbound_aggregate() const override { return false; }
+  bool is_bound_aggregate() const override { return false; }
+

Review Comment:
   ```suggestion
   ```
   
   We don't need to override them here.



-- 
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: issues-unsubscribe@iceberg.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org


Re: [PR] feat: add aggregate expressions and evaluator [iceberg-cpp]

Posted by "Jinchul81 (via GitHub)" <gi...@apache.org>.
Jinchul81 commented on code in PR #335:
URL: https://github.com/apache/iceberg-cpp/pull/335#discussion_r2553963003


##########
src/iceberg/expression/aggregate.cc:
##########
@@ -0,0 +1,289 @@
+/*
+ * 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/aggregate.h"
+
+#include <format>
+#include <optional>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/expression/expression.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/type.h"
+#include "iceberg/util/checked_cast.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+std::string OperationToPrefix(Expression::Operation op) {

Review Comment:
   Please make the return type `constexpr std::string_view`.



-- 
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: issues-unsubscribe@iceberg.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org


Re: [PR] feat: add aggregate expressions and evaluator [iceberg-cpp]

Posted by "SuKi2cn (via GitHub)" <gi...@apache.org>.
SuKi2cn commented on code in PR #335:
URL: https://github.com/apache/iceberg-cpp/pull/335#discussion_r2553979461


##########
src/iceberg/expression/aggregate.cc:
##########
@@ -0,0 +1,289 @@
+/*
+ * 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/aggregate.h"
+
+#include <format>
+#include <optional>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/expression/expression.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/type.h"
+#include "iceberg/util/checked_cast.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+std::string OperationToPrefix(Expression::Operation op) {
+  switch (op) {
+    case Expression::Operation::kMax:
+      return "max";
+    case Expression::Operation::kMin:
+      return "min";
+    case Expression::Operation::kCount:
+    case Expression::Operation::kCountStar:
+      return "count";
+    default:
+      break;
+  }
+  return "aggregate";
+}
+
+Result<std::shared_ptr<PrimitiveType>> GetPrimitiveType(const BoundTerm& term) {
+  auto primitive = std::dynamic_pointer_cast<PrimitiveType>(term.type());
+  if (primitive == nullptr) {
+    return InvalidExpression("Aggregate requires primitive type, got {}",
+                             term.type()->ToString());
+  }
+  return primitive;
+}
+
+}  // namespace
+
+CountAggregate::CountAggregate(Expression::Operation op, Mode mode,
+                               std::shared_ptr<UnboundTerm<BoundReference>> term,
+                               std::shared_ptr<NamedReference> reference)
+    : UnboundAggregate(op),
+      mode_(mode),
+      term_(std::move(term)),
+      reference_(std::move(reference)) {}
+
+Result<std::unique_ptr<CountAggregate>> CountAggregate::Count(
+    std::shared_ptr<UnboundTerm<BoundReference>> term) {
+  auto ref = term->reference();
+  return std::unique_ptr<CountAggregate>(new CountAggregate(
+      Expression::Operation::kCount, Mode::kNonNull, std::move(term), std::move(ref)));
+}
+
+Result<std::unique_ptr<CountAggregate>> CountAggregate::CountNull(
+    std::shared_ptr<UnboundTerm<BoundReference>> term) {
+  auto ref = term->reference();
+  return std::unique_ptr<CountAggregate>(new CountAggregate(
+      Expression::Operation::kCount, Mode::kNull, std::move(term), std::move(ref)));
+}
+
+std::unique_ptr<CountAggregate> CountAggregate::CountStar() {
+  return std::unique_ptr<CountAggregate>(new CountAggregate(
+      Expression::Operation::kCountStar, Mode::kStar, nullptr, nullptr));
+}
+
+std::string CountAggregate::ToString() const {
+  if (mode_ == Mode::kStar) {
+    return "count(*)";
+  }
+  ICEBERG_DCHECK(reference_ != nullptr, "Count aggregate should have reference");
+  switch (mode_) {
+    case Mode::kNull:
+      return std::format("count_null({})", reference_->name());
+    case Mode::kNonNull:
+      return std::format("count({})", reference_->name());
+    case Mode::kStar:
+      break;
+  }
+  std::unreachable();
+}
+
+Result<std::shared_ptr<Expression>> CountAggregate::Bind(const Schema& schema,
+                                                         bool case_sensitive) const {
+  std::shared_ptr<BoundTerm> bound_term;
+  if (term_ != nullptr) {
+    ICEBERG_ASSIGN_OR_THROW(auto bound, term_->Bind(schema, case_sensitive));
+    bound_term = std::move(bound);
+  }
+  auto aggregate =
+      std::make_shared<BoundCountAggregate>(op(), mode_, std::move(bound_term));
+  return aggregate;
+}
+
+BoundCountAggregate::BoundCountAggregate(Expression::Operation op,
+                                         CountAggregate::Mode mode,
+                                         std::shared_ptr<BoundTerm> term)
+    : BoundAggregate(op, std::move(term)), mode_(mode) {}
+
+std::string BoundCountAggregate::ToString() const {
+  if (mode_ == CountAggregate::Mode::kStar) {

Review Comment:
   > Let's set aside the idea of copying the Java implementation. Can you please clarify why we cannot make it as the generic approach?
   
   Got it, thanks for clarifying.
   
   I understand your point. The goal here is not to blindly mirror the Java implementation, but to design a more generic and idiomatic C++ solution.
   
   I'll take some time to revisit the current design and see how `kStar` can be modeled in a more generic way (e.g. reducing special-casing in `ToString()` and improving consistency with the Predicate style), while still preserving the same external semantics.
   
   I’ll follow up with an updated implementation once I’ve reworked this part.
   



-- 
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: issues-unsubscribe@iceberg.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org


Re: [PR] feat: add aggregate expressions and evaluator [iceberg-cpp]

Posted by "SuKi2cn (via GitHub)" <gi...@apache.org>.
SuKi2cn commented on PR #335:
URL: https://github.com/apache/iceberg-cpp/pull/335#issuecomment-3587794263

   > Thanks for working on this, @SuKi2cn! This looks good to me.
   
   Thanks a lot for the review and guidance, @wgtmac ! I really appreciate it. Happy Thanksgiving!
   


-- 
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: issues-unsubscribe@iceberg.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org


Re: [PR] feat: add aggregate expressions and evaluator [iceberg-cpp]

Posted by "SuKi2cn (via GitHub)" <gi...@apache.org>.
SuKi2cn commented on code in PR #335:
URL: https://github.com/apache/iceberg-cpp/pull/335#discussion_r2564049820


##########
src/iceberg/expression/aggregate.cc:
##########
@@ -0,0 +1,398 @@
+/*
+ * 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/aggregate.h"
+
+#include <format>
+#include <optional>
+#include <vector>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/type.h"
+#include "iceberg/util/checked_cast.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+Result<std::shared_ptr<PrimitiveType>> GetPrimitiveType(const BoundTerm& term) {
+  if (!term.type()->is_primitive()) {
+    return InvalidExpression("Aggregate requires primitive type, got {}",
+                             term.type()->ToString());
+  }
+  return internal::checked_pointer_cast<PrimitiveType>(term.type());
+}
+
+class CountNonNullAggregator : public BoundAggregate::Aggregator {
+ public:
+  explicit CountNonNullAggregator(std::shared_ptr<BoundTerm> term)
+      : term_(std::move(term)) {}
+
+  Status Update(const StructLike& row) override {
+    ICEBERG_ASSIGN_OR_RAISE(auto literal, term_->Evaluate(row));
+    if (!literal.IsNull()) {
+      ++count_;
+    }
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override { return Literal::Long(count_); }
+
+ private:
+  std::shared_ptr<BoundTerm> term_;
+  int64_t count_ = 0;
+};
+
+class CountNullAggregator : public BoundAggregate::Aggregator {
+ public:
+  explicit CountNullAggregator(std::shared_ptr<BoundTerm> term)
+      : term_(std::move(term)) {}
+
+  Status Update(const StructLike& row) override {
+    ICEBERG_ASSIGN_OR_RAISE(auto literal, term_->Evaluate(row));
+    if (literal.IsNull()) {
+      ++count_;
+    }
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override { return Literal::Long(count_); }
+
+ private:
+  std::shared_ptr<BoundTerm> term_;
+  int64_t count_ = 0;
+};
+
+class CountStarAggregator : public BoundAggregate::Aggregator {
+ public:
+  Status Update(const StructLike& /*row*/) override {
+    ++count_;
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override { return Literal::Long(count_); }
+
+ private:
+  int64_t count_ = 0;
+};
+
+class ValueAggregatorImpl : public BoundAggregate::Aggregator {
+ public:
+  ValueAggregatorImpl(bool is_max, std::shared_ptr<BoundTerm> term)
+      : is_max_(is_max), term_(std::move(term)) {}
+
+  Status Update(const StructLike& row) override {
+    ICEBERG_ASSIGN_OR_RAISE(auto val_literal, term_->Evaluate(row));
+    if (val_literal.IsNull()) {
+      return {};
+    }
+    if (!current_) {
+      current_ = std::move(val_literal);
+      return {};
+    }
+
+    auto ordering = val_literal <=> *current_;
+    if (ordering == std::partial_ordering::unordered) {
+      return InvalidExpression("Cannot compare literals of type {}",
+                               val_literal.type()->ToString());
+    }
+
+    if (is_max_) {
+      if (ordering == std::partial_ordering::greater) {
+        current_ = std::move(val_literal);
+      }
+    } else {
+      if (ordering == std::partial_ordering::less) {
+        current_ = std::move(val_literal);
+      }
+    }
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override {
+    if (current_) {
+      return *current_;
+    }
+    ICEBERG_ASSIGN_OR_RAISE(auto type, GetPrimitiveType(*term_));
+    return Literal::Null(type);
+  }
+
+ private:
+  bool is_max_;
+  std::shared_ptr<BoundTerm> term_;
+  std::optional<Literal> current_;
+};
+
+}  // namespace
+
+// -------------------- Bound aggregates --------------------
+
+CountNonNullAggregate::CountNonNullAggregate(std::shared_ptr<BoundTerm> term)
+    : CountAggregate(Expression::Operation::kCount, std::move(term)) {}
+
+Result<std::shared_ptr<CountNonNullAggregate>> CountNonNullAggregate::Make(
+    std::shared_ptr<BoundTerm> term) {
+  if (!term) {
+    return InvalidExpression("Bound count aggregate requires non-null term");
+  }
+  return std::shared_ptr<CountNonNullAggregate>(
+      new CountNonNullAggregate(std::move(term)));
+}
+
+std::string CountNonNullAggregate::ToString() const {
+  ICEBERG_DCHECK(term() != nullptr, "Bound count aggregate should have term");
+  return std::format("count({})", term()->reference()->name());
+}
+
+Result<Literal> CountNonNullAggregate::Evaluate(const StructLike& data) const {
+  ICEBERG_ASSIGN_OR_RAISE(auto literal, term()->Evaluate(data));
+  return Literal::Long(literal.IsNull() ? 0 : 1);
+}
+
+Result<std::unique_ptr<BoundAggregate::Aggregator>> CountNonNullAggregate::NewAggregator()
+    const {
+  return std::unique_ptr<BoundAggregate::Aggregator>(new CountNonNullAggregator(term()));
+}
+
+CountNullAggregate::CountNullAggregate(std::shared_ptr<BoundTerm> term)
+    : CountAggregate(Expression::Operation::kCountNull, std::move(term)) {}
+
+Result<std::shared_ptr<CountNullAggregate>> CountNullAggregate::Make(
+    std::shared_ptr<BoundTerm> term) {
+  if (!term) {
+    return InvalidExpression("Bound count aggregate requires non-null term");
+  }
+  return std::shared_ptr<CountNullAggregate>(new CountNullAggregate(std::move(term)));
+}
+
+std::string CountNullAggregate::ToString() const {
+  ICEBERG_DCHECK(term() != nullptr, "Bound count aggregate should have term");
+  return std::format("count_null({})", term()->reference()->name());
+}
+
+Result<Literal> CountNullAggregate::Evaluate(const StructLike& data) const {
+  ICEBERG_ASSIGN_OR_RAISE(auto literal, term()->Evaluate(data));
+  return Literal::Long(literal.IsNull() ? 1 : 0);
+}
+
+Result<std::unique_ptr<BoundAggregate::Aggregator>> CountNullAggregate::NewAggregator()
+    const {
+  return std::unique_ptr<BoundAggregate::Aggregator>(new CountNullAggregator(term()));
+}
+
+CountStarAggregate::CountStarAggregate()
+    : CountAggregate(Expression::Operation::kCountStar, nullptr) {}
+
+Result<std::shared_ptr<CountStarAggregate>> CountStarAggregate::Make() {
+  return std::shared_ptr<CountStarAggregate>(new CountStarAggregate());
+}
+
+std::string CountStarAggregate::ToString() const { return "count(*)"; }
+
+Result<Literal> CountStarAggregate::Evaluate(const StructLike& data) const {
+  return Literal::Long(1);
+}
+
+Result<std::unique_ptr<BoundAggregate::Aggregator>> CountStarAggregate::NewAggregator()
+    const {
+  return std::unique_ptr<BoundAggregate::Aggregator>(new CountStarAggregator());
+}
+
+ValueAggregate::ValueAggregate(Expression::Operation op, std::shared_ptr<BoundTerm> term)
+    : BoundAggregate(op, std::move(term)) {}
+
+std::string ValueAggregate::ToString() const {
+  ICEBERG_DCHECK(term() != nullptr, "Bound value aggregate should have term");
+  auto prefix = op() == Expression::Operation::kMax ? "max" : "min";
+  return std::format("{}({})", prefix, term()->reference()->name());
+}
+
+Result<Literal> ValueAggregate::Evaluate(const StructLike& data) const {
+  ICEBERG_ASSIGN_OR_RAISE(auto literal, term()->Evaluate(data));
+  return literal;
+}
+
+Result<std::unique_ptr<BoundAggregate::Aggregator>> ValueAggregate::NewAggregator()
+    const {
+  bool is_max = op() == Expression::Operation::kMax;
+  return std::unique_ptr<BoundAggregate::Aggregator>(
+      new ValueAggregatorImpl(is_max, term()));
+}
+
+// -------------------- Unbound binding --------------------
+
+template <typename B>
+Result<std::shared_ptr<Expression>> UnboundAggregateImpl<B>::Bind(
+    const Schema& schema, bool case_sensitive) const {
+  ICEBERG_DCHECK(UnboundAggregateImpl<B>::IsSupportedOp(this->op()),
+                 "Unexpected aggregate operation");
+
+  std::shared_ptr<B> bound_term;
+  if (this->term()) {
+    ICEBERG_ASSIGN_OR_THROW(bound_term, this->term()->Bind(schema, case_sensitive));
+  }
+
+  switch (this->op()) {
+    case Expression::Operation::kCountStar: {
+      ICEBERG_ASSIGN_OR_THROW(auto aggregate, CountStarAggregate::Make());
+      return aggregate;
+    }
+    case Expression::Operation::kCount: {
+      if (!bound_term) {

Review Comment:
   > Shouldn't this be checked by CountNonNullAggregate::Make?
   
   I can't agree more. This check belongs in `CountNonNullAggregate::Make`, since a non-null term is part of the aggregate’s own invariant. Moving it there centralizes validation, avoids duplicating the same logic in `Bind`, and ensures any future callers of `Make()` also get the correct error handling automatically.



-- 
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: issues-unsubscribe@iceberg.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org


Re: [PR] feat: add aggregate expressions and evaluator [iceberg-cpp]

Posted by "SuKi2cn (via GitHub)" <gi...@apache.org>.
SuKi2cn commented on code in PR #335:
URL: https://github.com/apache/iceberg-cpp/pull/335#discussion_r2563967863


##########
src/iceberg/expression/aggregate.cc:
##########
@@ -0,0 +1,398 @@
+/*
+ * 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/aggregate.h"
+
+#include <format>
+#include <optional>
+#include <vector>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/type.h"
+#include "iceberg/util/checked_cast.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+Result<std::shared_ptr<PrimitiveType>> GetPrimitiveType(const BoundTerm& term) {
+  if (!term.type()->is_primitive()) {
+    return InvalidExpression("Aggregate requires primitive type, got {}",
+                             term.type()->ToString());
+  }
+  return internal::checked_pointer_cast<PrimitiveType>(term.type());
+}
+
+class CountNonNullAggregator : public BoundAggregate::Aggregator {
+ public:
+  explicit CountNonNullAggregator(std::shared_ptr<BoundTerm> term)
+      : term_(std::move(term)) {}
+
+  Status Update(const StructLike& row) override {
+    ICEBERG_ASSIGN_OR_RAISE(auto literal, term_->Evaluate(row));
+    if (!literal.IsNull()) {
+      ++count_;
+    }
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override { return Literal::Long(count_); }
+
+ private:
+  std::shared_ptr<BoundTerm> term_;
+  int64_t count_ = 0;
+};
+
+class CountNullAggregator : public BoundAggregate::Aggregator {
+ public:
+  explicit CountNullAggregator(std::shared_ptr<BoundTerm> term)
+      : term_(std::move(term)) {}
+
+  Status Update(const StructLike& row) override {
+    ICEBERG_ASSIGN_OR_RAISE(auto literal, term_->Evaluate(row));
+    if (literal.IsNull()) {
+      ++count_;
+    }
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override { return Literal::Long(count_); }
+
+ private:
+  std::shared_ptr<BoundTerm> term_;
+  int64_t count_ = 0;
+};
+
+class CountStarAggregator : public BoundAggregate::Aggregator {
+ public:
+  Status Update(const StructLike& /*row*/) override {
+    ++count_;
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override { return Literal::Long(count_); }
+
+ private:
+  int64_t count_ = 0;
+};
+
+class ValueAggregatorImpl : public BoundAggregate::Aggregator {
+ public:
+  ValueAggregatorImpl(bool is_max, std::shared_ptr<BoundTerm> term)
+      : is_max_(is_max), term_(std::move(term)) {}
+
+  Status Update(const StructLike& row) override {
+    ICEBERG_ASSIGN_OR_RAISE(auto val_literal, term_->Evaluate(row));
+    if (val_literal.IsNull()) {
+      return {};
+    }
+    if (!current_) {
+      current_ = std::move(val_literal);
+      return {};
+    }
+
+    auto ordering = val_literal <=> *current_;
+    if (ordering == std::partial_ordering::unordered) {
+      return InvalidExpression("Cannot compare literals of type {}",
+                               val_literal.type()->ToString());
+    }
+
+    if (is_max_) {
+      if (ordering == std::partial_ordering::greater) {
+        current_ = std::move(val_literal);
+      }
+    } else {
+      if (ordering == std::partial_ordering::less) {
+        current_ = std::move(val_literal);
+      }
+    }
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override {
+    if (current_) {
+      return *current_;
+    }
+    ICEBERG_ASSIGN_OR_RAISE(auto type, GetPrimitiveType(*term_));
+    return Literal::Null(type);
+  }
+
+ private:
+  bool is_max_;
+  std::shared_ptr<BoundTerm> term_;
+  std::optional<Literal> current_;
+};
+
+}  // namespace
+
+// -------------------- Bound aggregates --------------------
+
+CountNonNullAggregate::CountNonNullAggregate(std::shared_ptr<BoundTerm> term)
+    : CountAggregate(Expression::Operation::kCount, std::move(term)) {}
+
+Result<std::shared_ptr<CountNonNullAggregate>> CountNonNullAggregate::Make(

Review Comment:
   > 
   
   
   
   > Same for other Make functions.
   
   Applied this suggestion and changed all `Make` functions to return `std::unique_ptr`, converting to `std::shared_ptr` at the call sites where shared ownership is actually required.



-- 
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: issues-unsubscribe@iceberg.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org


Re: [PR] feat: add aggregate expressions and evaluator [iceberg-cpp]

Posted by "Jinchul81 (via GitHub)" <gi...@apache.org>.
Jinchul81 commented on code in PR #335:
URL: https://github.com/apache/iceberg-cpp/pull/335#discussion_r2553196780


##########
src/iceberg/expression/aggregate.cc:
##########
@@ -0,0 +1,289 @@
+/*
+ * 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/aggregate.h"
+
+#include <format>
+#include <optional>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/expression/expression.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/type.h"
+#include "iceberg/util/checked_cast.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+std::string OperationToPrefix(Expression::Operation op) {

Review Comment:
   Why don't we use `std::string_view` instead of `std::string` if the return values should be constant string literals?



##########
src/iceberg/expression/expressions.cc:
##########
@@ -81,6 +82,63 @@ std::shared_ptr<UnboundTransform> Expressions::Transform(
   return unbound_transform;
 }
 
+// Aggregates
+
+std::shared_ptr<CountAggregate> Expressions::Count(std::string name) {
+  return Count(Ref(std::move(name)));
+}
+
+std::shared_ptr<CountAggregate> Expressions::Count(
+    std::shared_ptr<UnboundTerm<BoundReference>> expr) {
+  ICEBERG_ASSIGN_OR_THROW(auto agg, CountAggregate::Count(std::move(expr)));
+  return std::shared_ptr<CountAggregate>(std::move(agg));
+}
+
+std::shared_ptr<CountAggregate> Expressions::CountNull(std::string name) {
+  return CountNull(Ref(std::move(name)));
+}
+
+std::shared_ptr<CountAggregate> Expressions::CountNull(
+    std::shared_ptr<UnboundTerm<BoundReference>> expr) {
+  ICEBERG_ASSIGN_OR_THROW(auto agg, CountAggregate::CountNull(std::move(expr)));
+  return std::shared_ptr<CountAggregate>(std::move(agg));
+}
+
+std::shared_ptr<CountAggregate> Expressions::CountNotNull(std::string name) {
+  return CountNotNull(Ref(std::move(name)));
+}
+
+std::shared_ptr<CountAggregate> Expressions::CountNotNull(
+    std::shared_ptr<UnboundTerm<BoundReference>> expr) {
+  ICEBERG_ASSIGN_OR_THROW(auto agg, CountAggregate::Count(std::move(expr)));
+  return std::shared_ptr<CountAggregate>(std::move(agg));
+}
+
+std::shared_ptr<CountAggregate> Expressions::CountStar() {
+  auto agg = CountAggregate::CountStar();
+  return std::shared_ptr<CountAggregate>(std::move(agg));
+}
+
+std::shared_ptr<ValueAggregate> Expressions::Max(std::string name) {

Review Comment:
   Same above.
   



##########
src/iceberg/expression/aggregate.cc:
##########
@@ -0,0 +1,289 @@
+/*
+ * 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/aggregate.h"
+
+#include <format>
+#include <optional>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/expression/expression.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/type.h"
+#include "iceberg/util/checked_cast.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+std::string OperationToPrefix(Expression::Operation op) {
+  switch (op) {
+    case Expression::Operation::kMax:
+      return "max";
+    case Expression::Operation::kMin:
+      return "min";
+    case Expression::Operation::kCount:
+    case Expression::Operation::kCountStar:
+      return "count";
+    default:
+      break;
+  }
+  return "aggregate";
+}
+
+Result<std::shared_ptr<PrimitiveType>> GetPrimitiveType(const BoundTerm& term) {
+  auto primitive = std::dynamic_pointer_cast<PrimitiveType>(term.type());
+  if (primitive == nullptr) {
+    return InvalidExpression("Aggregate requires primitive type, got {}",
+                             term.type()->ToString());
+  }
+  return primitive;
+}
+
+}  // namespace
+
+CountAggregate::CountAggregate(Expression::Operation op, Mode mode,
+                               std::shared_ptr<UnboundTerm<BoundReference>> term,
+                               std::shared_ptr<NamedReference> reference)
+    : UnboundAggregate(op),
+      mode_(mode),
+      term_(std::move(term)),
+      reference_(std::move(reference)) {}
+
+Result<std::unique_ptr<CountAggregate>> CountAggregate::Count(
+    std::shared_ptr<UnboundTerm<BoundReference>> term) {
+  auto ref = term->reference();
+  return std::unique_ptr<CountAggregate>(new CountAggregate(
+      Expression::Operation::kCount, Mode::kNonNull, std::move(term), std::move(ref)));
+}
+
+Result<std::unique_ptr<CountAggregate>> CountAggregate::CountNull(
+    std::shared_ptr<UnboundTerm<BoundReference>> term) {
+  auto ref = term->reference();
+  return std::unique_ptr<CountAggregate>(new CountAggregate(
+      Expression::Operation::kCount, Mode::kNull, std::move(term), std::move(ref)));
+}
+
+std::unique_ptr<CountAggregate> CountAggregate::CountStar() {
+  return std::unique_ptr<CountAggregate>(new CountAggregate(
+      Expression::Operation::kCountStar, Mode::kStar, nullptr, nullptr));
+}
+
+std::string CountAggregate::ToString() const {
+  if (mode_ == Mode::kStar) {
+    return "count(*)";
+  }
+  ICEBERG_DCHECK(reference_ != nullptr, "Count aggregate should have reference");
+  switch (mode_) {
+    case Mode::kNull:
+      return std::format("count_null({})", reference_->name());
+    case Mode::kNonNull:
+      return std::format("count({})", reference_->name());
+    case Mode::kStar:
+      break;
+  }
+  std::unreachable();
+}
+
+Result<std::shared_ptr<Expression>> CountAggregate::Bind(const Schema& schema,
+                                                         bool case_sensitive) const {
+  std::shared_ptr<BoundTerm> bound_term;
+  if (term_ != nullptr) {
+    ICEBERG_ASSIGN_OR_THROW(auto bound, term_->Bind(schema, case_sensitive));
+    bound_term = std::move(bound);
+  }
+  auto aggregate =
+      std::make_shared<BoundCountAggregate>(op(), mode_, std::move(bound_term));
+  return aggregate;
+}
+
+BoundCountAggregate::BoundCountAggregate(Expression::Operation op,
+                                         CountAggregate::Mode mode,
+                                         std::shared_ptr<BoundTerm> term)
+    : BoundAggregate(op, std::move(term)), mode_(mode) {}
+
+std::string BoundCountAggregate::ToString() const {
+  if (mode_ == CountAggregate::Mode::kStar) {
+    return "count(*)";
+  }
+  ICEBERG_DCHECK(term() != nullptr, "Bound count aggregate should have term");
+  switch (mode_) {
+    case CountAggregate::Mode::kNull:
+      return std::format("count_null({})", term()->reference()->name());
+    case CountAggregate::Mode::kNonNull:
+      return std::format("count({})", term()->reference()->name());
+    case CountAggregate::Mode::kStar:
+      break;
+  }
+  std::unreachable();
+}
+
+ValueAggregate::ValueAggregate(Expression::Operation op,
+                               std::shared_ptr<UnboundTerm<BoundReference>> term,
+                               std::shared_ptr<NamedReference> reference)
+    : UnboundAggregate(op), term_(std::move(term)), reference_(std::move(reference)) {}
+
+Result<std::unique_ptr<ValueAggregate>> ValueAggregate::Max(
+    std::shared_ptr<UnboundTerm<BoundReference>> term) {
+  auto ref = term->reference();
+  return std::unique_ptr<ValueAggregate>(
+      new ValueAggregate(Expression::Operation::kMax, std::move(term), std::move(ref)));
+}
+
+Result<std::unique_ptr<ValueAggregate>> ValueAggregate::Min(
+    std::shared_ptr<UnboundTerm<BoundReference>> term) {
+  auto ref = term->reference();
+  return std::unique_ptr<ValueAggregate>(
+      new ValueAggregate(Expression::Operation::kMin, std::move(term), std::move(ref)));
+}
+
+std::string ValueAggregate::ToString() const {
+  return std::format("{}({})", OperationToPrefix(op()), reference_->name());
+}
+
+Result<std::shared_ptr<Expression>> ValueAggregate::Bind(const Schema& schema,
+                                                         bool case_sensitive) const {
+  ICEBERG_ASSIGN_OR_THROW(auto bound, term_->Bind(schema, case_sensitive));
+  auto aggregate = std::make_shared<BoundValueAggregate>(
+      op(), std::shared_ptr<BoundTerm>(std::move(bound)));
+  return aggregate;
+}
+
+BoundValueAggregate::BoundValueAggregate(Expression::Operation op,
+                                         std::shared_ptr<BoundTerm> term)
+    : BoundAggregate(op, std::move(term)) {}
+
+std::string BoundValueAggregate::ToString() const {
+  ICEBERG_DCHECK(term() != nullptr, "Bound value aggregate should have term");
+  return std::format("{}({})", OperationToPrefix(op()), term()->reference()->name());
+}
+
+namespace {
+
+class CountEvaluator : public AggregateEvaluator {
+ public:
+  CountEvaluator(CountAggregate::Mode mode, std::shared_ptr<BoundTerm> term)
+      : mode_(mode), term_(std::move(term)) {}
+
+  Status Add(const StructLike& row) override {
+    switch (mode_) {
+      case CountAggregate::Mode::kStar:
+        ++count_;
+        return {};
+      case CountAggregate::Mode::kNonNull: {
+        ICEBERG_ASSIGN_OR_RAISE(auto literal, term_->Evaluate(row));
+        if (!literal.IsNull()) {
+          ++count_;
+        }
+        return {};
+      }
+      case CountAggregate::Mode::kNull: {
+        ICEBERG_ASSIGN_OR_RAISE(auto literal, term_->Evaluate(row));
+        if (literal.IsNull()) {
+          ++count_;
+        }
+        return {};
+      }
+    }
+    std::unreachable();
+  }
+
+  Result<Literal> ResultLiteral() const override { return Literal::Long(count_); }
+
+ private:
+  CountAggregate::Mode mode_;
+  std::shared_ptr<BoundTerm> term_;
+  int64_t count_ = 0;
+};
+
+class ValueAggregateEvaluator : public AggregateEvaluator {
+ public:
+  ValueAggregateEvaluator(Expression::Operation op, std::shared_ptr<BoundTerm> term,
+                          std::shared_ptr<PrimitiveType> type)
+      : op_(op), term_(std::move(term)), type_(std::move(type)) {}
+
+  Status Add(const StructLike& row) override {
+    ICEBERG_ASSIGN_OR_RAISE(auto literal, term_->Evaluate(row));
+    if (literal.IsNull()) {
+      return {};
+    }
+
+    if (!current_) {
+      current_ = std::move(literal);
+      return {};
+    }
+
+    auto ordering = literal <=> *current_;
+    if (ordering == std::partial_ordering::unordered) {
+      return InvalidExpression("Cannot compare literals of type {}",
+                               literal.type()->ToString());
+    }
+
+    if (op_ == Expression::Operation::kMax) {

Review Comment:
   Please add a comment why `current_` should be overwritten here.



##########
src/iceberg/expression/expressions.cc:
##########
@@ -81,6 +82,63 @@ std::shared_ptr<UnboundTransform> Expressions::Transform(
   return unbound_transform;
 }
 
+// Aggregates
+
+std::shared_ptr<CountAggregate> Expressions::Count(std::string name) {
+  return Count(Ref(std::move(name)));
+}
+
+std::shared_ptr<CountAggregate> Expressions::Count(
+    std::shared_ptr<UnboundTerm<BoundReference>> expr) {
+  ICEBERG_ASSIGN_OR_THROW(auto agg, CountAggregate::Count(std::move(expr)));
+  return std::shared_ptr<CountAggregate>(std::move(agg));
+}
+
+std::shared_ptr<CountAggregate> Expressions::CountNull(std::string name) {
+  return CountNull(Ref(std::move(name)));
+}
+
+std::shared_ptr<CountAggregate> Expressions::CountNull(
+    std::shared_ptr<UnboundTerm<BoundReference>> expr) {
+  ICEBERG_ASSIGN_OR_THROW(auto agg, CountAggregate::CountNull(std::move(expr)));
+  return std::shared_ptr<CountAggregate>(std::move(agg));

Review Comment:
   It looks like return `std::move(agg));` is enough.



##########
src/iceberg/expression/aggregate.h:
##########
@@ -0,0 +1,186 @@
+/*
+ * 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/aggregate.h
+/// Aggregate expression definitions.
+
+#include <memory>
+
+#include "iceberg/expression/expression.h"
+#include "iceberg/expression/term.h"
+#include "iceberg/result.h"
+
+namespace iceberg {
+
+class AggregateEvaluator;
+
+/// \brief Base class for aggregate expressions.
+class ICEBERG_EXPORT Aggregate : public Expression {
+ public:
+  ~Aggregate() override = default;
+
+  Expression::Operation op() const override { return operation_; }
+
+  bool is_unbound_aggregate() const override { return false; }
+  bool is_bound_aggregate() const override { return false; }
+
+ protected:
+  explicit Aggregate(Expression::Operation op) : operation_(op) {}
+
+ private:
+  Expression::Operation operation_;
+};
+
+/// \brief Unbound aggregate with an optional term.
+class ICEBERG_EXPORT UnboundAggregate : public Aggregate, public Unbound<Expression> {
+ public:
+  ~UnboundAggregate() override = default;
+
+  bool is_unbound_aggregate() const override { return true; }
+
+  /// \brief Returns the unbound reference if the aggregate has a term.
+  virtual std::shared_ptr<NamedReference> reference() override = 0;
+
+ protected:
+  explicit UnboundAggregate(Expression::Operation op) : Aggregate(op) {}
+};
+
+/// \brief Bound aggregate with an optional term.
+class ICEBERG_EXPORT BoundAggregate : public Aggregate {
+ public:
+  ~BoundAggregate() override = default;
+
+  bool is_bound_aggregate() const override { return true; }
+
+  const std::shared_ptr<BoundTerm>& term() const { return term_; }
+
+ protected:
+  BoundAggregate(Expression::Operation op, std::shared_ptr<BoundTerm> term)
+      : Aggregate(op), term_(std::move(term)) {}
+
+ private:
+  std::shared_ptr<BoundTerm> term_;
+};
+
+/// \brief COUNT aggregate variants.
+class ICEBERG_EXPORT CountAggregate : public UnboundAggregate {
+ public:
+  enum class Mode { kNonNull, kNull, kStar };
+
+  static Result<std::unique_ptr<CountAggregate>> Count(
+      std::shared_ptr<UnboundTerm<BoundReference>> term);
+
+  static Result<std::unique_ptr<CountAggregate>> CountNull(
+      std::shared_ptr<UnboundTerm<BoundReference>> term);
+
+  static std::unique_ptr<CountAggregate> CountStar();
+
+  ~CountAggregate() override = default;
+
+  Mode mode() const { return mode_; }
+
+  const std::shared_ptr<UnboundTerm<BoundReference>>& term() const { return term_; }
+
+  std::shared_ptr<NamedReference> reference() override { return reference_; }
+
+  std::string ToString() const override;
+
+  Result<std::shared_ptr<Expression>> Bind(const Schema& schema,
+                                           bool case_sensitive) const override;
+
+ private:
+  CountAggregate(Expression::Operation op, Mode mode,

Review Comment:
   Please add a comment why public ctor is not allowed.



##########
src/iceberg/expression/aggregate.cc:
##########
@@ -0,0 +1,289 @@
+/*
+ * 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/aggregate.h"
+
+#include <format>
+#include <optional>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/expression/expression.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/type.h"
+#include "iceberg/util/checked_cast.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+std::string OperationToPrefix(Expression::Operation op) {
+  switch (op) {
+    case Expression::Operation::kMax:
+      return "max";
+    case Expression::Operation::kMin:
+      return "min";
+    case Expression::Operation::kCount:
+    case Expression::Operation::kCountStar:
+      return "count";
+    default:
+      break;
+  }
+  return "aggregate";
+}
+
+Result<std::shared_ptr<PrimitiveType>> GetPrimitiveType(const BoundTerm& term) {
+  auto primitive = std::dynamic_pointer_cast<PrimitiveType>(term.type());
+  if (primitive == nullptr) {
+    return InvalidExpression("Aggregate requires primitive type, got {}",
+                             term.type()->ToString());
+  }
+  return primitive;
+}
+
+}  // namespace
+
+CountAggregate::CountAggregate(Expression::Operation op, Mode mode,
+                               std::shared_ptr<UnboundTerm<BoundReference>> term,
+                               std::shared_ptr<NamedReference> reference)
+    : UnboundAggregate(op),
+      mode_(mode),
+      term_(std::move(term)),
+      reference_(std::move(reference)) {}
+
+Result<std::unique_ptr<CountAggregate>> CountAggregate::Count(
+    std::shared_ptr<UnboundTerm<BoundReference>> term) {
+  auto ref = term->reference();
+  return std::unique_ptr<CountAggregate>(new CountAggregate(
+      Expression::Operation::kCount, Mode::kNonNull, std::move(term), std::move(ref)));
+}
+
+Result<std::unique_ptr<CountAggregate>> CountAggregate::CountNull(
+    std::shared_ptr<UnboundTerm<BoundReference>> term) {
+  auto ref = term->reference();
+  return std::unique_ptr<CountAggregate>(new CountAggregate(
+      Expression::Operation::kCount, Mode::kNull, std::move(term), std::move(ref)));
+}
+
+std::unique_ptr<CountAggregate> CountAggregate::CountStar() {
+  return std::unique_ptr<CountAggregate>(new CountAggregate(
+      Expression::Operation::kCountStar, Mode::kStar, nullptr, nullptr));
+}
+
+std::string CountAggregate::ToString() const {
+  if (mode_ == Mode::kStar) {
+    return "count(*)";
+  }
+  ICEBERG_DCHECK(reference_ != nullptr, "Count aggregate should have reference");
+  switch (mode_) {
+    case Mode::kNull:
+      return std::format("count_null({})", reference_->name());
+    case Mode::kNonNull:
+      return std::format("count({})", reference_->name());
+    case Mode::kStar:
+      break;
+  }
+  std::unreachable();
+}
+
+Result<std::shared_ptr<Expression>> CountAggregate::Bind(const Schema& schema,
+                                                         bool case_sensitive) const {
+  std::shared_ptr<BoundTerm> bound_term;
+  if (term_ != nullptr) {
+    ICEBERG_ASSIGN_OR_THROW(auto bound, term_->Bind(schema, case_sensitive));
+    bound_term = std::move(bound);
+  }
+  auto aggregate =
+      std::make_shared<BoundCountAggregate>(op(), mode_, std::move(bound_term));
+  return aggregate;
+}
+
+BoundCountAggregate::BoundCountAggregate(Expression::Operation op,
+                                         CountAggregate::Mode mode,
+                                         std::shared_ptr<BoundTerm> term)
+    : BoundAggregate(op, std::move(term)), mode_(mode) {}
+
+std::string BoundCountAggregate::ToString() const {
+  if (mode_ == CountAggregate::Mode::kStar) {

Review Comment:
   Why did you special handling for `kStar`?
   



##########
src/iceberg/expression/expressions.cc:
##########
@@ -81,6 +82,63 @@ std::shared_ptr<UnboundTransform> Expressions::Transform(
   return unbound_transform;
 }
 
+// Aggregates
+
+std::shared_ptr<CountAggregate> Expressions::Count(std::string name) {
+  return Count(Ref(std::move(name)));
+}
+
+std::shared_ptr<CountAggregate> Expressions::Count(
+    std::shared_ptr<UnboundTerm<BoundReference>> expr) {
+  ICEBERG_ASSIGN_OR_THROW(auto agg, CountAggregate::Count(std::move(expr)));
+  return std::shared_ptr<CountAggregate>(std::move(agg));
+}
+
+std::shared_ptr<CountAggregate> Expressions::CountNull(std::string name) {

Review Comment:
   Same above.



##########
src/iceberg/expression/expressions.cc:
##########
@@ -81,6 +82,63 @@ std::shared_ptr<UnboundTransform> Expressions::Transform(
   return unbound_transform;
 }
 
+// Aggregates
+
+std::shared_ptr<CountAggregate> Expressions::Count(std::string name) {
+  return Count(Ref(std::move(name)));
+}
+
+std::shared_ptr<CountAggregate> Expressions::Count(
+    std::shared_ptr<UnboundTerm<BoundReference>> expr) {
+  ICEBERG_ASSIGN_OR_THROW(auto agg, CountAggregate::Count(std::move(expr)));
+  return std::shared_ptr<CountAggregate>(std::move(agg));
+}
+
+std::shared_ptr<CountAggregate> Expressions::CountNull(std::string name) {
+  return CountNull(Ref(std::move(name)));
+}
+
+std::shared_ptr<CountAggregate> Expressions::CountNull(
+    std::shared_ptr<UnboundTerm<BoundReference>> expr) {
+  ICEBERG_ASSIGN_OR_THROW(auto agg, CountAggregate::CountNull(std::move(expr)));
+  return std::shared_ptr<CountAggregate>(std::move(agg));
+}
+
+std::shared_ptr<CountAggregate> Expressions::CountNotNull(std::string name) {
+  return CountNotNull(Ref(std::move(name)));
+}
+
+std::shared_ptr<CountAggregate> Expressions::CountNotNull(
+    std::shared_ptr<UnboundTerm<BoundReference>> expr) {
+  ICEBERG_ASSIGN_OR_THROW(auto agg, CountAggregate::Count(std::move(expr)));
+  return std::shared_ptr<CountAggregate>(std::move(agg));
+}
+
+std::shared_ptr<CountAggregate> Expressions::CountStar() {
+  auto agg = CountAggregate::CountStar();
+  return std::shared_ptr<CountAggregate>(std::move(agg));
+}
+
+std::shared_ptr<ValueAggregate> Expressions::Max(std::string name) {
+  return Max(Ref(std::move(name)));
+}
+
+std::shared_ptr<ValueAggregate> Expressions::Max(
+    std::shared_ptr<UnboundTerm<BoundReference>> expr) {
+  ICEBERG_ASSIGN_OR_THROW(auto agg, ValueAggregate::Max(std::move(expr)));
+  return std::shared_ptr<ValueAggregate>(std::move(agg));
+}
+
+std::shared_ptr<ValueAggregate> Expressions::Min(std::string name) {
+  return Min(Ref(std::move(name)));
+}
+
+std::shared_ptr<ValueAggregate> Expressions::Min(
+    std::shared_ptr<UnboundTerm<BoundReference>> expr) {
+  ICEBERG_ASSIGN_OR_THROW(auto agg, ValueAggregate::Min(std::move(expr)));
+  return std::shared_ptr<ValueAggregate>(std::move(agg));

Review Comment:
   It looks like `return std::move(agg));` is enough.



##########
src/iceberg/expression/expressions.cc:
##########
@@ -81,6 +82,63 @@ std::shared_ptr<UnboundTransform> Expressions::Transform(
   return unbound_transform;
 }
 
+// Aggregates
+
+std::shared_ptr<CountAggregate> Expressions::Count(std::string name) {

Review Comment:
   You can change the parameter to `std::string&&` to avoid an unnecessary copy and clearly express that the function intends to take ownership of the string. Using an rvalue reference makes the API’s intent clearer and can reduce overhead in performance-critical paths.



##########
src/iceberg/expression/expressions.cc:
##########
@@ -81,6 +82,63 @@ std::shared_ptr<UnboundTransform> Expressions::Transform(
   return unbound_transform;
 }
 
+// Aggregates
+
+std::shared_ptr<CountAggregate> Expressions::Count(std::string name) {
+  return Count(Ref(std::move(name)));
+}
+
+std::shared_ptr<CountAggregate> Expressions::Count(
+    std::shared_ptr<UnboundTerm<BoundReference>> expr) {
+  ICEBERG_ASSIGN_OR_THROW(auto agg, CountAggregate::Count(std::move(expr)));
+  return std::shared_ptr<CountAggregate>(std::move(agg));
+}
+
+std::shared_ptr<CountAggregate> Expressions::CountNull(std::string name) {
+  return CountNull(Ref(std::move(name)));
+}
+
+std::shared_ptr<CountAggregate> Expressions::CountNull(
+    std::shared_ptr<UnboundTerm<BoundReference>> expr) {
+  ICEBERG_ASSIGN_OR_THROW(auto agg, CountAggregate::CountNull(std::move(expr)));
+  return std::shared_ptr<CountAggregate>(std::move(agg));
+}
+
+std::shared_ptr<CountAggregate> Expressions::CountNotNull(std::string name) {

Review Comment:
   Same above.
   



##########
src/iceberg/expression/expression_visitor.h:
##########
@@ -77,6 +78,22 @@ class ICEBERG_EXPORT ExpressionVisitor {
   /// \brief Visit an unbound predicate.
   /// \param pred The unbound predicate to visit
   virtual Result<R> Predicate(const std::shared_ptr<UnboundPredicate>& pred) = 0;
+
+  /// \brief Visit a bound aggregate.
+  /// \param aggregate The bound aggregate to visit.
+  virtual Result<R> Aggregate(const std::shared_ptr<BoundAggregate>& aggregate) {
+    ICEBERG_DCHECK(aggregate != nullptr, "Bound aggregate cannot be null");
+    return NotSupported("Bound aggregate is not supported by this visitor: {}",
+                        aggregate->ToString());
+  }
+
+  /// \brief Visit an unbound aggregate.
+  /// \param aggregate The unbound aggregate to visit.
+  virtual Result<R> Aggregate(const std::shared_ptr<UnboundAggregate>& aggregate) {

Review Comment:
   Please add comment for the return.



##########
src/iceberg/expression/expression_visitor.h:
##########
@@ -77,6 +78,22 @@ class ICEBERG_EXPORT ExpressionVisitor {
   /// \brief Visit an unbound predicate.
   /// \param pred The unbound predicate to visit
   virtual Result<R> Predicate(const std::shared_ptr<UnboundPredicate>& pred) = 0;
+
+  /// \brief Visit a bound aggregate.
+  /// \param aggregate The bound aggregate to visit.

Review Comment:
   Please add comment for the return.



##########
src/iceberg/expression/aggregate.cc:
##########
@@ -0,0 +1,289 @@
+/*
+ * 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/aggregate.h"
+
+#include <format>
+#include <optional>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/expression/expression.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/type.h"
+#include "iceberg/util/checked_cast.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+std::string OperationToPrefix(Expression::Operation op) {
+  switch (op) {
+    case Expression::Operation::kMax:
+      return "max";
+    case Expression::Operation::kMin:
+      return "min";
+    case Expression::Operation::kCount:
+    case Expression::Operation::kCountStar:
+      return "count";
+    default:
+      break;
+  }
+  return "aggregate";
+}
+
+Result<std::shared_ptr<PrimitiveType>> GetPrimitiveType(const BoundTerm& term) {
+  auto primitive = std::dynamic_pointer_cast<PrimitiveType>(term.type());
+  if (primitive == nullptr) {
+    return InvalidExpression("Aggregate requires primitive type, got {}",
+                             term.type()->ToString());
+  }
+  return primitive;
+}
+
+}  // namespace
+
+CountAggregate::CountAggregate(Expression::Operation op, Mode mode,
+                               std::shared_ptr<UnboundTerm<BoundReference>> term,
+                               std::shared_ptr<NamedReference> reference)
+    : UnboundAggregate(op),
+      mode_(mode),
+      term_(std::move(term)),
+      reference_(std::move(reference)) {}
+
+Result<std::unique_ptr<CountAggregate>> CountAggregate::Count(
+    std::shared_ptr<UnboundTerm<BoundReference>> term) {
+  auto ref = term->reference();
+  return std::unique_ptr<CountAggregate>(new CountAggregate(
+      Expression::Operation::kCount, Mode::kNonNull, std::move(term), std::move(ref)));
+}
+
+Result<std::unique_ptr<CountAggregate>> CountAggregate::CountNull(
+    std::shared_ptr<UnboundTerm<BoundReference>> term) {
+  auto ref = term->reference();
+  return std::unique_ptr<CountAggregate>(new CountAggregate(
+      Expression::Operation::kCount, Mode::kNull, std::move(term), std::move(ref)));
+}
+
+std::unique_ptr<CountAggregate> CountAggregate::CountStar() {
+  return std::unique_ptr<CountAggregate>(new CountAggregate(
+      Expression::Operation::kCountStar, Mode::kStar, nullptr, nullptr));
+}
+
+std::string CountAggregate::ToString() const {
+  if (mode_ == Mode::kStar) {
+    return "count(*)";
+  }
+  ICEBERG_DCHECK(reference_ != nullptr, "Count aggregate should have reference");
+  switch (mode_) {
+    case Mode::kNull:
+      return std::format("count_null({})", reference_->name());
+    case Mode::kNonNull:
+      return std::format("count({})", reference_->name());
+    case Mode::kStar:
+      break;
+  }
+  std::unreachable();
+}
+
+Result<std::shared_ptr<Expression>> CountAggregate::Bind(const Schema& schema,
+                                                         bool case_sensitive) const {
+  std::shared_ptr<BoundTerm> bound_term;
+  if (term_ != nullptr) {
+    ICEBERG_ASSIGN_OR_THROW(auto bound, term_->Bind(schema, case_sensitive));
+    bound_term = std::move(bound);
+  }
+  auto aggregate =
+      std::make_shared<BoundCountAggregate>(op(), mode_, std::move(bound_term));
+  return aggregate;
+}
+
+BoundCountAggregate::BoundCountAggregate(Expression::Operation op,
+                                         CountAggregate::Mode mode,
+                                         std::shared_ptr<BoundTerm> term)
+    : BoundAggregate(op, std::move(term)), mode_(mode) {}
+
+std::string BoundCountAggregate::ToString() const {
+  if (mode_ == CountAggregate::Mode::kStar) {
+    return "count(*)";
+  }
+  ICEBERG_DCHECK(term() != nullptr, "Bound count aggregate should have term");
+  switch (mode_) {
+    case CountAggregate::Mode::kNull:
+      return std::format("count_null({})", term()->reference()->name());
+    case CountAggregate::Mode::kNonNull:
+      return std::format("count({})", term()->reference()->name());
+    case CountAggregate::Mode::kStar:
+      break;
+  }
+  std::unreachable();
+}
+
+ValueAggregate::ValueAggregate(Expression::Operation op,
+                               std::shared_ptr<UnboundTerm<BoundReference>> term,
+                               std::shared_ptr<NamedReference> reference)
+    : UnboundAggregate(op), term_(std::move(term)), reference_(std::move(reference)) {}
+
+Result<std::unique_ptr<ValueAggregate>> ValueAggregate::Max(
+    std::shared_ptr<UnboundTerm<BoundReference>> term) {
+  auto ref = term->reference();
+  return std::unique_ptr<ValueAggregate>(
+      new ValueAggregate(Expression::Operation::kMax, std::move(term), std::move(ref)));
+}
+
+Result<std::unique_ptr<ValueAggregate>> ValueAggregate::Min(
+    std::shared_ptr<UnboundTerm<BoundReference>> term) {
+  auto ref = term->reference();
+  return std::unique_ptr<ValueAggregate>(
+      new ValueAggregate(Expression::Operation::kMin, std::move(term), std::move(ref)));
+}
+
+std::string ValueAggregate::ToString() const {
+  return std::format("{}({})", OperationToPrefix(op()), reference_->name());
+}
+
+Result<std::shared_ptr<Expression>> ValueAggregate::Bind(const Schema& schema,
+                                                         bool case_sensitive) const {
+  ICEBERG_ASSIGN_OR_THROW(auto bound, term_->Bind(schema, case_sensitive));
+  auto aggregate = std::make_shared<BoundValueAggregate>(
+      op(), std::shared_ptr<BoundTerm>(std::move(bound)));
+  return aggregate;
+}
+
+BoundValueAggregate::BoundValueAggregate(Expression::Operation op,
+                                         std::shared_ptr<BoundTerm> term)
+    : BoundAggregate(op, std::move(term)) {}
+
+std::string BoundValueAggregate::ToString() const {
+  ICEBERG_DCHECK(term() != nullptr, "Bound value aggregate should have term");
+  return std::format("{}({})", OperationToPrefix(op()), term()->reference()->name());
+}
+
+namespace {
+
+class CountEvaluator : public AggregateEvaluator {
+ public:
+  CountEvaluator(CountAggregate::Mode mode, std::shared_ptr<BoundTerm> term)
+      : mode_(mode), term_(std::move(term)) {}
+
+  Status Add(const StructLike& row) override {
+    switch (mode_) {
+      case CountAggregate::Mode::kStar:
+        ++count_;
+        return {};
+      case CountAggregate::Mode::kNonNull: {
+        ICEBERG_ASSIGN_OR_RAISE(auto literal, term_->Evaluate(row));
+        if (!literal.IsNull()) {
+          ++count_;
+        }
+        return {};
+      }
+      case CountAggregate::Mode::kNull: {
+        ICEBERG_ASSIGN_OR_RAISE(auto literal, term_->Evaluate(row));
+        if (literal.IsNull()) {
+          ++count_;
+        }
+        return {};
+      }
+    }
+    std::unreachable();
+  }
+
+  Result<Literal> ResultLiteral() const override { return Literal::Long(count_); }
+
+ private:
+  CountAggregate::Mode mode_;
+  std::shared_ptr<BoundTerm> term_;
+  int64_t count_ = 0;
+};
+
+class ValueAggregateEvaluator : public AggregateEvaluator {
+ public:
+  ValueAggregateEvaluator(Expression::Operation op, std::shared_ptr<BoundTerm> term,
+                          std::shared_ptr<PrimitiveType> type)
+      : op_(op), term_(std::move(term)), type_(std::move(type)) {}
+
+  Status Add(const StructLike& row) override {
+    ICEBERG_ASSIGN_OR_RAISE(auto literal, term_->Evaluate(row));
+    if (literal.IsNull()) {
+      return {};
+    }
+
+    if (!current_) {
+      current_ = std::move(literal);
+      return {};
+    }
+
+    auto ordering = literal <=> *current_;
+    if (ordering == std::partial_ordering::unordered) {

Review Comment:
   Please pull up this condition. We can exit early if the conditions are not satisfied.



##########
src/iceberg/expression/expressions.cc:
##########
@@ -81,6 +82,63 @@ std::shared_ptr<UnboundTransform> Expressions::Transform(
   return unbound_transform;
 }
 
+// Aggregates
+
+std::shared_ptr<CountAggregate> Expressions::Count(std::string name) {
+  return Count(Ref(std::move(name)));
+}
+
+std::shared_ptr<CountAggregate> Expressions::Count(
+    std::shared_ptr<UnboundTerm<BoundReference>> expr) {
+  ICEBERG_ASSIGN_OR_THROW(auto agg, CountAggregate::Count(std::move(expr)));
+  return std::shared_ptr<CountAggregate>(std::move(agg));
+}
+
+std::shared_ptr<CountAggregate> Expressions::CountNull(std::string name) {
+  return CountNull(Ref(std::move(name)));
+}
+
+std::shared_ptr<CountAggregate> Expressions::CountNull(
+    std::shared_ptr<UnboundTerm<BoundReference>> expr) {
+  ICEBERG_ASSIGN_OR_THROW(auto agg, CountAggregate::CountNull(std::move(expr)));
+  return std::shared_ptr<CountAggregate>(std::move(agg));
+}
+
+std::shared_ptr<CountAggregate> Expressions::CountNotNull(std::string name) {
+  return CountNotNull(Ref(std::move(name)));
+}
+
+std::shared_ptr<CountAggregate> Expressions::CountNotNull(
+    std::shared_ptr<UnboundTerm<BoundReference>> expr) {
+  ICEBERG_ASSIGN_OR_THROW(auto agg, CountAggregate::Count(std::move(expr)));
+  return std::shared_ptr<CountAggregate>(std::move(agg));
+}
+
+std::shared_ptr<CountAggregate> Expressions::CountStar() {
+  auto agg = CountAggregate::CountStar();
+  return std::shared_ptr<CountAggregate>(std::move(agg));
+}
+
+std::shared_ptr<ValueAggregate> Expressions::Max(std::string name) {
+  return Max(Ref(std::move(name)));
+}
+
+std::shared_ptr<ValueAggregate> Expressions::Max(
+    std::shared_ptr<UnboundTerm<BoundReference>> expr) {
+  ICEBERG_ASSIGN_OR_THROW(auto agg, ValueAggregate::Max(std::move(expr)));
+  return std::shared_ptr<ValueAggregate>(std::move(agg));
+}
+
+std::shared_ptr<ValueAggregate> Expressions::Min(std::string name) {

Review Comment:
   Same above.
   



##########
src/iceberg/test/aggregate_test.cc:
##########
@@ -0,0 +1,132 @@
+/*
+ * 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/aggregate.h"
+
+#include <gtest/gtest.h>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/expression/expressions.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/schema.h"
+#include "iceberg/test/matchers.h"
+#include "iceberg/type.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+class VectorStructLike : public StructLike {
+ public:
+  explicit VectorStructLike(std::vector<Scalar> fields) : fields_(std::move(fields)) {}
+
+  Result<Scalar> GetField(size_t pos) const override {
+    if (pos >= fields_.size()) {
+      return InvalidArgument("Position {} out of range", pos);

Review Comment:
   Please also show the number of fields to find out the cause easily.



##########
src/iceberg/expression/expressions.cc:
##########
@@ -81,6 +82,63 @@ std::shared_ptr<UnboundTransform> Expressions::Transform(
   return unbound_transform;
 }
 
+// Aggregates
+
+std::shared_ptr<CountAggregate> Expressions::Count(std::string name) {
+  return Count(Ref(std::move(name)));
+}
+
+std::shared_ptr<CountAggregate> Expressions::Count(
+    std::shared_ptr<UnboundTerm<BoundReference>> expr) {
+  ICEBERG_ASSIGN_OR_THROW(auto agg, CountAggregate::Count(std::move(expr)));
+  return std::shared_ptr<CountAggregate>(std::move(agg));
+}
+
+std::shared_ptr<CountAggregate> Expressions::CountNull(std::string name) {
+  return CountNull(Ref(std::move(name)));
+}
+
+std::shared_ptr<CountAggregate> Expressions::CountNull(
+    std::shared_ptr<UnboundTerm<BoundReference>> expr) {
+  ICEBERG_ASSIGN_OR_THROW(auto agg, CountAggregate::CountNull(std::move(expr)));
+  return std::shared_ptr<CountAggregate>(std::move(agg));
+}
+
+std::shared_ptr<CountAggregate> Expressions::CountNotNull(std::string name) {
+  return CountNotNull(Ref(std::move(name)));
+}
+
+std::shared_ptr<CountAggregate> Expressions::CountNotNull(
+    std::shared_ptr<UnboundTerm<BoundReference>> expr) {
+  ICEBERG_ASSIGN_OR_THROW(auto agg, CountAggregate::Count(std::move(expr)));
+  return std::shared_ptr<CountAggregate>(std::move(agg));
+}
+
+std::shared_ptr<CountAggregate> Expressions::CountStar() {
+  auto agg = CountAggregate::CountStar();
+  return std::shared_ptr<CountAggregate>(std::move(agg));
+}
+
+std::shared_ptr<ValueAggregate> Expressions::Max(std::string name) {
+  return Max(Ref(std::move(name)));
+}
+
+std::shared_ptr<ValueAggregate> Expressions::Max(
+    std::shared_ptr<UnboundTerm<BoundReference>> expr) {
+  ICEBERG_ASSIGN_OR_THROW(auto agg, ValueAggregate::Max(std::move(expr)));
+  return std::shared_ptr<ValueAggregate>(std::move(agg));

Review Comment:
   It looks like return `std::move(agg));` is enough.



##########
src/iceberg/expression/aggregate.h:
##########
@@ -0,0 +1,186 @@
+/*
+ * 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/aggregate.h
+/// Aggregate expression definitions.
+
+#include <memory>
+
+#include "iceberg/expression/expression.h"
+#include "iceberg/expression/term.h"
+#include "iceberg/result.h"
+
+namespace iceberg {
+
+class AggregateEvaluator;
+
+/// \brief Base class for aggregate expressions.
+class ICEBERG_EXPORT Aggregate : public Expression {
+ public:
+  ~Aggregate() override = default;
+
+  Expression::Operation op() const override { return operation_; }
+
+  bool is_unbound_aggregate() const override { return false; }
+  bool is_bound_aggregate() const override { return false; }
+
+ protected:
+  explicit Aggregate(Expression::Operation op) : operation_(op) {}
+
+ private:
+  Expression::Operation operation_;
+};
+
+/// \brief Unbound aggregate with an optional term.
+class ICEBERG_EXPORT UnboundAggregate : public Aggregate, public Unbound<Expression> {
+ public:
+  ~UnboundAggregate() override = default;
+
+  bool is_unbound_aggregate() const override { return true; }
+
+  /// \brief Returns the unbound reference if the aggregate has a term.
+  virtual std::shared_ptr<NamedReference> reference() override = 0;
+
+ protected:
+  explicit UnboundAggregate(Expression::Operation op) : Aggregate(op) {}
+};
+
+/// \brief Bound aggregate with an optional term.

Review Comment:
   What's the definition of term here? Why does aggregate optionally require term? Please put this into the comment.



##########
src/iceberg/expression/aggregate.h:
##########
@@ -0,0 +1,186 @@
+/*
+ * 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/aggregate.h
+/// Aggregate expression definitions.
+
+#include <memory>
+
+#include "iceberg/expression/expression.h"
+#include "iceberg/expression/term.h"
+#include "iceberg/result.h"
+
+namespace iceberg {
+
+class AggregateEvaluator;
+
+/// \brief Base class for aggregate expressions.
+class ICEBERG_EXPORT Aggregate : public Expression {
+ public:
+  ~Aggregate() override = default;
+
+  Expression::Operation op() const override { return operation_; }
+
+  bool is_unbound_aggregate() const override { return false; }
+  bool is_bound_aggregate() const override { return false; }
+
+ protected:
+  explicit Aggregate(Expression::Operation op) : operation_(op) {}
+
+ private:
+  Expression::Operation operation_;
+};
+
+/// \brief Unbound aggregate with an optional term.
+class ICEBERG_EXPORT UnboundAggregate : public Aggregate, public Unbound<Expression> {
+ public:
+  ~UnboundAggregate() override = default;
+
+  bool is_unbound_aggregate() const override { return true; }
+
+  /// \brief Returns the unbound reference if the aggregate has a term.
+  virtual std::shared_ptr<NamedReference> reference() override = 0;
+
+ protected:
+  explicit UnboundAggregate(Expression::Operation op) : Aggregate(op) {}
+};
+
+/// \brief Bound aggregate with an optional term.
+class ICEBERG_EXPORT BoundAggregate : public Aggregate {
+ public:
+  ~BoundAggregate() override = default;
+
+  bool is_bound_aggregate() const override { return true; }
+
+  const std::shared_ptr<BoundTerm>& term() const { return term_; }
+
+ protected:
+  BoundAggregate(Expression::Operation op, std::shared_ptr<BoundTerm> term)
+      : Aggregate(op), term_(std::move(term)) {}
+
+ private:
+  std::shared_ptr<BoundTerm> term_;
+};
+
+/// \brief COUNT aggregate variants.
+class ICEBERG_EXPORT CountAggregate : public UnboundAggregate {
+ public:
+  enum class Mode { kNonNull, kNull, kStar };

Review Comment:
   Please add a comment for the description for them.



##########
src/iceberg/expression/aggregate.cc:
##########
@@ -0,0 +1,289 @@
+/*
+ * 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/aggregate.h"
+
+#include <format>
+#include <optional>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/expression/expression.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/type.h"
+#include "iceberg/util/checked_cast.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+std::string OperationToPrefix(Expression::Operation op) {
+  switch (op) {
+    case Expression::Operation::kMax:
+      return "max";
+    case Expression::Operation::kMin:
+      return "min";
+    case Expression::Operation::kCount:
+    case Expression::Operation::kCountStar:
+      return "count";
+    default:
+      break;
+  }
+  return "aggregate";
+}
+
+Result<std::shared_ptr<PrimitiveType>> GetPrimitiveType(const BoundTerm& term) {
+  auto primitive = std::dynamic_pointer_cast<PrimitiveType>(term.type());
+  if (primitive == nullptr) {
+    return InvalidExpression("Aggregate requires primitive type, got {}",
+                             term.type()->ToString());
+  }
+  return primitive;
+}
+
+}  // namespace
+
+CountAggregate::CountAggregate(Expression::Operation op, Mode mode,
+                               std::shared_ptr<UnboundTerm<BoundReference>> term,
+                               std::shared_ptr<NamedReference> reference)
+    : UnboundAggregate(op),
+      mode_(mode),
+      term_(std::move(term)),
+      reference_(std::move(reference)) {}
+
+Result<std::unique_ptr<CountAggregate>> CountAggregate::Count(
+    std::shared_ptr<UnboundTerm<BoundReference>> term) {
+  auto ref = term->reference();
+  return std::unique_ptr<CountAggregate>(new CountAggregate(
+      Expression::Operation::kCount, Mode::kNonNull, std::move(term), std::move(ref)));
+}
+
+Result<std::unique_ptr<CountAggregate>> CountAggregate::CountNull(
+    std::shared_ptr<UnboundTerm<BoundReference>> term) {
+  auto ref = term->reference();
+  return std::unique_ptr<CountAggregate>(new CountAggregate(
+      Expression::Operation::kCount, Mode::kNull, std::move(term), std::move(ref)));
+}
+
+std::unique_ptr<CountAggregate> CountAggregate::CountStar() {

Review Comment:
   Why does it return `std::unique_ptr<CountAggregate>` instead of `Result<std::unique_ptr<CountAggregate>>`?



##########
src/iceberg/expression/expressions.cc:
##########
@@ -81,6 +82,63 @@ std::shared_ptr<UnboundTransform> Expressions::Transform(
   return unbound_transform;
 }
 
+// Aggregates
+
+std::shared_ptr<CountAggregate> Expressions::Count(std::string name) {
+  return Count(Ref(std::move(name)));
+}
+
+std::shared_ptr<CountAggregate> Expressions::Count(
+    std::shared_ptr<UnboundTerm<BoundReference>> expr) {
+  ICEBERG_ASSIGN_OR_THROW(auto agg, CountAggregate::Count(std::move(expr)));
+  return std::shared_ptr<CountAggregate>(std::move(agg));

Review Comment:
   It looks like return `std::move(agg));` is enough.



##########
src/iceberg/expression/expressions.cc:
##########
@@ -81,6 +82,63 @@ std::shared_ptr<UnboundTransform> Expressions::Transform(
   return unbound_transform;
 }
 
+// Aggregates
+
+std::shared_ptr<CountAggregate> Expressions::Count(std::string name) {
+  return Count(Ref(std::move(name)));
+}
+
+std::shared_ptr<CountAggregate> Expressions::Count(
+    std::shared_ptr<UnboundTerm<BoundReference>> expr) {
+  ICEBERG_ASSIGN_OR_THROW(auto agg, CountAggregate::Count(std::move(expr)));
+  return std::shared_ptr<CountAggregate>(std::move(agg));
+}
+
+std::shared_ptr<CountAggregate> Expressions::CountNull(std::string name) {
+  return CountNull(Ref(std::move(name)));
+}
+
+std::shared_ptr<CountAggregate> Expressions::CountNull(
+    std::shared_ptr<UnboundTerm<BoundReference>> expr) {
+  ICEBERG_ASSIGN_OR_THROW(auto agg, CountAggregate::CountNull(std::move(expr)));
+  return std::shared_ptr<CountAggregate>(std::move(agg));
+}
+
+std::shared_ptr<CountAggregate> Expressions::CountNotNull(std::string name) {
+  return CountNotNull(Ref(std::move(name)));
+}
+
+std::shared_ptr<CountAggregate> Expressions::CountNotNull(
+    std::shared_ptr<UnboundTerm<BoundReference>> expr) {
+  ICEBERG_ASSIGN_OR_THROW(auto agg, CountAggregate::Count(std::move(expr)));
+  return std::shared_ptr<CountAggregate>(std::move(agg));
+}
+
+std::shared_ptr<CountAggregate> Expressions::CountStar() {
+  auto agg = CountAggregate::CountStar();
+  return std::shared_ptr<CountAggregate>(std::move(agg));

Review Comment:
   It looks like return `std::move(agg));` is enough.



##########
src/iceberg/expression/expressions.cc:
##########
@@ -81,6 +82,63 @@ std::shared_ptr<UnboundTransform> Expressions::Transform(
   return unbound_transform;
 }
 
+// Aggregates
+
+std::shared_ptr<CountAggregate> Expressions::Count(std::string name) {
+  return Count(Ref(std::move(name)));
+}
+
+std::shared_ptr<CountAggregate> Expressions::Count(
+    std::shared_ptr<UnboundTerm<BoundReference>> expr) {
+  ICEBERG_ASSIGN_OR_THROW(auto agg, CountAggregate::Count(std::move(expr)));
+  return std::shared_ptr<CountAggregate>(std::move(agg));
+}
+
+std::shared_ptr<CountAggregate> Expressions::CountNull(std::string name) {
+  return CountNull(Ref(std::move(name)));
+}
+
+std::shared_ptr<CountAggregate> Expressions::CountNull(
+    std::shared_ptr<UnboundTerm<BoundReference>> expr) {
+  ICEBERG_ASSIGN_OR_THROW(auto agg, CountAggregate::CountNull(std::move(expr)));
+  return std::shared_ptr<CountAggregate>(std::move(agg));
+}
+
+std::shared_ptr<CountAggregate> Expressions::CountNotNull(std::string name) {
+  return CountNotNull(Ref(std::move(name)));
+}
+
+std::shared_ptr<CountAggregate> Expressions::CountNotNull(
+    std::shared_ptr<UnboundTerm<BoundReference>> expr) {
+  ICEBERG_ASSIGN_OR_THROW(auto agg, CountAggregate::Count(std::move(expr)));
+  return std::shared_ptr<CountAggregate>(std::move(agg));

Review Comment:
   It looks like return `std::move(agg));` is enough.



##########
src/iceberg/expression/aggregate.cc:
##########
@@ -0,0 +1,289 @@
+/*
+ * 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/aggregate.h"
+
+#include <format>
+#include <optional>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/expression/expression.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/type.h"
+#include "iceberg/util/checked_cast.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+std::string OperationToPrefix(Expression::Operation op) {
+  switch (op) {
+    case Expression::Operation::kMax:
+      return "max";
+    case Expression::Operation::kMin:
+      return "min";
+    case Expression::Operation::kCount:
+    case Expression::Operation::kCountStar:
+      return "count";
+    default:
+      break;
+  }
+  return "aggregate";

Review Comment:
   I don't think this is the right return value. Why do we have to map the others to `aggregate`? 



##########
src/iceberg/expression/aggregate.cc:
##########
@@ -0,0 +1,289 @@
+/*
+ * 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/aggregate.h"
+
+#include <format>
+#include <optional>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/expression/expression.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/type.h"
+#include "iceberg/util/checked_cast.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+std::string OperationToPrefix(Expression::Operation op) {
+  switch (op) {
+    case Expression::Operation::kMax:
+      return "max";
+    case Expression::Operation::kMin:
+      return "min";
+    case Expression::Operation::kCount:
+    case Expression::Operation::kCountStar:
+      return "count";
+    default:
+      break;
+  }
+  return "aggregate";
+}
+
+Result<std::shared_ptr<PrimitiveType>> GetPrimitiveType(const BoundTerm& term) {
+  auto primitive = std::dynamic_pointer_cast<PrimitiveType>(term.type());
+  if (primitive == nullptr) {
+    return InvalidExpression("Aggregate requires primitive type, got {}",
+                             term.type()->ToString());
+  }
+  return primitive;
+}
+
+}  // namespace
+
+CountAggregate::CountAggregate(Expression::Operation op, Mode mode,
+                               std::shared_ptr<UnboundTerm<BoundReference>> term,
+                               std::shared_ptr<NamedReference> reference)
+    : UnboundAggregate(op),
+      mode_(mode),
+      term_(std::move(term)),
+      reference_(std::move(reference)) {}
+
+Result<std::unique_ptr<CountAggregate>> CountAggregate::Count(
+    std::shared_ptr<UnboundTerm<BoundReference>> term) {
+  auto ref = term->reference();
+  return std::unique_ptr<CountAggregate>(new CountAggregate(
+      Expression::Operation::kCount, Mode::kNonNull, std::move(term), std::move(ref)));
+}
+
+Result<std::unique_ptr<CountAggregate>> CountAggregate::CountNull(
+    std::shared_ptr<UnboundTerm<BoundReference>> term) {
+  auto ref = term->reference();
+  return std::unique_ptr<CountAggregate>(new CountAggregate(
+      Expression::Operation::kCount, Mode::kNull, std::move(term), std::move(ref)));
+}
+
+std::unique_ptr<CountAggregate> CountAggregate::CountStar() {
+  return std::unique_ptr<CountAggregate>(new CountAggregate(
+      Expression::Operation::kCountStar, Mode::kStar, nullptr, nullptr));
+}
+
+std::string CountAggregate::ToString() const {
+  if (mode_ == Mode::kStar) {

Review Comment:
   Why did you special handling for `kStar`? 



-- 
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: issues-unsubscribe@iceberg.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org


Re: [PR] feat: add aggregate expressions and evaluator [iceberg-cpp]

Posted by "SuKi2cn (via GitHub)" <gi...@apache.org>.
SuKi2cn commented on code in PR #335:
URL: https://github.com/apache/iceberg-cpp/pull/335#discussion_r2553856043


##########
src/iceberg/expression/aggregate.cc:
##########
@@ -0,0 +1,289 @@
+/*
+ * 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/aggregate.h"
+
+#include <format>
+#include <optional>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/expression/expression.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/type.h"
+#include "iceberg/util/checked_cast.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+std::string OperationToPrefix(Expression::Operation op) {
+  switch (op) {
+    case Expression::Operation::kMax:
+      return "max";
+    case Expression::Operation::kMin:
+      return "min";
+    case Expression::Operation::kCount:
+    case Expression::Operation::kCountStar:
+      return "count";
+    default:
+      break;
+  }
+  return "aggregate";
+}
+
+Result<std::shared_ptr<PrimitiveType>> GetPrimitiveType(const BoundTerm& term) {
+  auto primitive = std::dynamic_pointer_cast<PrimitiveType>(term.type());
+  if (primitive == nullptr) {
+    return InvalidExpression("Aggregate requires primitive type, got {}",
+                             term.type()->ToString());
+  }
+  return primitive;
+}
+
+}  // namespace
+
+CountAggregate::CountAggregate(Expression::Operation op, Mode mode,
+                               std::shared_ptr<UnboundTerm<BoundReference>> term,
+                               std::shared_ptr<NamedReference> reference)
+    : UnboundAggregate(op),
+      mode_(mode),
+      term_(std::move(term)),
+      reference_(std::move(reference)) {}
+
+Result<std::unique_ptr<CountAggregate>> CountAggregate::Count(
+    std::shared_ptr<UnboundTerm<BoundReference>> term) {
+  auto ref = term->reference();
+  return std::unique_ptr<CountAggregate>(new CountAggregate(
+      Expression::Operation::kCount, Mode::kNonNull, std::move(term), std::move(ref)));
+}
+
+Result<std::unique_ptr<CountAggregate>> CountAggregate::CountNull(
+    std::shared_ptr<UnboundTerm<BoundReference>> term) {
+  auto ref = term->reference();
+  return std::unique_ptr<CountAggregate>(new CountAggregate(
+      Expression::Operation::kCount, Mode::kNull, std::move(term), std::move(ref)));
+}
+
+std::unique_ptr<CountAggregate> CountAggregate::CountStar() {
+  return std::unique_ptr<CountAggregate>(new CountAggregate(
+      Expression::Operation::kCountStar, Mode::kStar, nullptr, nullptr));
+}
+
+std::string CountAggregate::ToString() const {
+  if (mode_ == Mode::kStar) {
+    return "count(*)";
+  }
+  ICEBERG_DCHECK(reference_ != nullptr, "Count aggregate should have reference");
+  switch (mode_) {
+    case Mode::kNull:
+      return std::format("count_null({})", reference_->name());
+    case Mode::kNonNull:
+      return std::format("count({})", reference_->name());
+    case Mode::kStar:
+      break;
+  }
+  std::unreachable();
+}
+
+Result<std::shared_ptr<Expression>> CountAggregate::Bind(const Schema& schema,
+                                                         bool case_sensitive) const {
+  std::shared_ptr<BoundTerm> bound_term;
+  if (term_ != nullptr) {
+    ICEBERG_ASSIGN_OR_THROW(auto bound, term_->Bind(schema, case_sensitive));
+    bound_term = std::move(bound);
+  }
+  auto aggregate =
+      std::make_shared<BoundCountAggregate>(op(), mode_, std::move(bound_term));
+  return aggregate;
+}
+
+BoundCountAggregate::BoundCountAggregate(Expression::Operation op,
+                                         CountAggregate::Mode mode,
+                                         std::shared_ptr<BoundTerm> term)
+    : BoundAggregate(op, std::move(term)), mode_(mode) {}
+
+std::string BoundCountAggregate::ToString() const {
+  if (mode_ == CountAggregate::Mode::kStar) {

Review Comment:
   > Why did you special handling for `kStar`?
   
   Good catch — mapping all remaining operations to `"aggregate"` is not really correct.
   
   In the Java implementation, `Aggregate.toString()` handles each aggregate operation explicitly and the `default` branch throws an `UnsupportedOperationException("Invalid aggregate: " + op())`, so there is no generic `"aggregate"` fallback.
   
   I’ll update the C++ helper to mirror that behavior: only the supported aggregate operations will be handled explicitly, and for anything else we’ll treat it as invalid/unreachable instead of returning `"aggregate"`. This way we don’t silently hide unsupported operations and stay consistent with the Java implementation.
   



-- 
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: issues-unsubscribe@iceberg.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org


Re: [PR] feat: add aggregate expressions and evaluator [iceberg-cpp]

Posted by "SuKi2cn (via GitHub)" <gi...@apache.org>.
SuKi2cn commented on code in PR #335:
URL: https://github.com/apache/iceberg-cpp/pull/335#discussion_r2564177103


##########
src/iceberg/expression/aggregate.h:
##########
@@ -0,0 +1,230 @@
+/*
+ * 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/aggregate.h
+/// Aggregate expression definitions.
+
+#include <memory>
+#include <optional>
+#include <string>
+#include <vector>
+
+#include "iceberg/expression/expression.h"
+#include "iceberg/expression/term.h"
+#include "iceberg/result.h"
+#include "iceberg/type_fwd.h"
+
+namespace iceberg {
+
+/// \brief Base aggregate holding an operation and a term.
+template <TermType T>
+class ICEBERG_EXPORT Aggregate : public virtual Expression {
+ public:
+  ~Aggregate() override = default;
+
+  Expression::Operation op() const override { return operation_; }
+
+  const std::shared_ptr<T>& term() const { return term_; }
+
+ protected:
+  Aggregate(Expression::Operation op, std::shared_ptr<T> term)
+      : operation_(op), term_(std::move(term)) {}
+
+  Expression::Operation operation_;
+  std::shared_ptr<T> term_;
+};
+
+/// \brief Base class for unbound aggregates.
+class ICEBERG_EXPORT UnboundAggregate : public virtual Expression,
+                                        public Unbound<Expression> {
+ public:
+  ~UnboundAggregate() override = default;
+
+  bool is_unbound_aggregate() const override { return true; }
+};
+
+/// \brief Template for unbound aggregates that carry a term and operation.
+template <typename B>
+class ICEBERG_EXPORT UnboundAggregateImpl : public UnboundAggregate,
+                                            public Aggregate<UnboundTerm<B>> {
+  using BASE = Aggregate<UnboundTerm<B>>;
+
+ public:
+  static Result<std::shared_ptr<UnboundAggregateImpl<B>>> Make(
+      Expression::Operation op, std::shared_ptr<UnboundTerm<B>> term);
+
+  std::shared_ptr<NamedReference> reference() override {
+    return BASE::term() ? BASE::term()->reference() : nullptr;
+  }
+
+  Result<std::shared_ptr<Expression>> Bind(const Schema& schema,
+                                           bool case_sensitive) const override;
+
+  std::string ToString() const override;
+
+ private:
+  static constexpr bool IsSupportedOp(Expression::Operation op) {
+    return op == Expression::Operation::kCount ||
+           op == Expression::Operation::kCountNull ||
+           op == Expression::Operation::kCountStar || op == Expression::Operation::kMax ||
+           op == Expression::Operation::kMin;
+  }
+
+  UnboundAggregateImpl(Expression::Operation op, std::shared_ptr<UnboundTerm<B>> term)
+      : BASE(op, std::move(term)) {
+    ICEBERG_DCHECK(IsSupportedOp(op), "Unexpected aggregate operation");
+    ICEBERG_DCHECK(op == Expression::Operation::kCountStar || BASE::term() != nullptr,
+                   "Aggregate term cannot be null unless COUNT(*)");
+  }
+};
+
+/// \brief Base class for bound aggregates.
+class ICEBERG_EXPORT BoundAggregate : public Aggregate<BoundTerm>, public Bound {
+ public:
+  using Aggregate<BoundTerm>::op;
+  using Aggregate<BoundTerm>::term;
+
+  class ICEBERG_EXPORT Aggregator {
+   public:
+    virtual ~Aggregator() = default;
+
+    virtual Status Update(const StructLike& row) = 0;
+    virtual Status Update(const DataFile& file) {
+      return NotSupported("Aggregating DataFile not supported");
+    }
+    virtual Result<Literal> ResultLiteral() const = 0;
+  };
+
+  std::shared_ptr<BoundReference> reference() override {
+    ICEBERG_DCHECK(term_ != nullptr || op() == Expression::Operation::kCountStar,
+                   "Bound aggregate term should not be null unless COUNT(*)");
+    return term_ ? term_->reference() : nullptr;
+  }
+
+  Result<Literal> Evaluate(const StructLike& data) const override = 0;
+
+  bool is_bound_aggregate() const override { return true; }
+
+  enum class Kind : int8_t {
+    // Count aggregates (COUNT, COUNT_STAR, COUNT_NULL)
+    kCount = 0,
+    // Value aggregates (MIN, MAX)
+    kValue,
+  };
+
+  virtual Kind kind() const = 0;
+  virtual Result<std::unique_ptr<Aggregator>> NewAggregator() const = 0;
+
+ protected:
+  BoundAggregate(Expression::Operation op, std::shared_ptr<BoundTerm> term)
+      : Aggregate<BoundTerm>(op, std::move(term)) {}
+};
+
+/// \brief Base class for COUNT aggregates.
+class ICEBERG_EXPORT CountAggregate : public BoundAggregate {
+ public:
+  Kind kind() const override { return Kind::kCount; }
+
+ protected:
+  CountAggregate(Expression::Operation op, std::shared_ptr<BoundTerm> term)
+      : BoundAggregate(op, std::move(term)) {}
+};
+
+/// \brief COUNT(term) aggregate.
+class ICEBERG_EXPORT CountNonNullAggregate : public CountAggregate {
+ public:
+  static Result<std::shared_ptr<CountNonNullAggregate>> Make(
+      std::shared_ptr<BoundTerm> term);
+
+  std::string ToString() const override;
+  Result<Literal> Evaluate(const StructLike& data) const override;
+  Result<std::unique_ptr<Aggregator>> NewAggregator() const override;
+
+ private:
+  explicit CountNonNullAggregate(std::shared_ptr<BoundTerm> term);
+};
+
+/// \brief COUNT_NULL(term) aggregate.
+class ICEBERG_EXPORT CountNullAggregate : public CountAggregate {
+ public:
+  static Result<std::shared_ptr<CountNullAggregate>> Make(
+      std::shared_ptr<BoundTerm> term);
+
+  std::string ToString() const override;
+  Result<Literal> Evaluate(const StructLike& data) const override;
+  Result<std::unique_ptr<Aggregator>> NewAggregator() const override;
+
+ private:
+  explicit CountNullAggregate(std::shared_ptr<BoundTerm> term);
+};
+
+/// \brief COUNT(*) aggregate.
+class ICEBERG_EXPORT CountStarAggregate : public CountAggregate {
+ public:
+  static Result<std::shared_ptr<CountStarAggregate>> Make();
+
+  std::string ToString() const override;
+  Result<Literal> Evaluate(const StructLike& data) const override;
+  Result<std::unique_ptr<Aggregator>> NewAggregator() const override;
+
+ private:
+  CountStarAggregate();
+};
+
+/// \brief Bound MAX/MIN aggregate.
+class ICEBERG_EXPORT ValueAggregate : public BoundAggregate {
+ public:
+  ValueAggregate(Expression::Operation op, std::shared_ptr<BoundTerm> term);
+
+  Kind kind() const override { return Kind::kValue; }
+
+  std::string ToString() const override;
+  Result<Literal> Evaluate(const StructLike& data) const override;
+  Result<std::unique_ptr<Aggregator>> NewAggregator() const override;
+};
+
+/// \brief Evaluates bound aggregates over StructLike rows.
+class ICEBERG_EXPORT AggregateEvaluator {
+ public:
+  virtual ~AggregateEvaluator() = default;
+
+  /// \brief Create an evaluator for a single bound aggregate.
+  /// \param aggregate The bound aggregate to evaluate across rows.
+  static Result<std::unique_ptr<AggregateEvaluator>> Make(
+      std::shared_ptr<BoundAggregate> aggregate);
+
+  /// \brief Create an evaluator for multiple bound aggregates.
+  /// \param aggregates Aggregates to evaluate in one pass; order is preserved in
+  /// Results().
+  static Result<std::unique_ptr<AggregateEvaluator>> MakeList(
+      std::vector<std::shared_ptr<BoundAggregate>> aggregates);
+
+  /// \brief Update aggregates with a row.
+  virtual Status Update(const StructLike& row) = 0;
+
+  /// \brief Final aggregated value.
+  virtual Result<std::vector<Literal>> Results() const = 0;

Review Comment:
   > Should we return `Result<std::span<const Literal>>`?
   
   Good suggestion. I’ve updated `Results()` to return `Result<std::span<const Literal>>` so callers get a lightweight read-only view without extra allocations or copies.



-- 
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: issues-unsubscribe@iceberg.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org


Re: [PR] feat: add aggregate expressions and evaluator [iceberg-cpp]

Posted by "SuKi2cn (via GitHub)" <gi...@apache.org>.
SuKi2cn commented on code in PR #335:
URL: https://github.com/apache/iceberg-cpp/pull/335#discussion_r2556969603


##########
src/iceberg/expression/aggregate.cc:
##########
@@ -0,0 +1,256 @@
+/*
+ * 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/aggregate.h"
+
+#include <format>
+#include <optional>
+#include <vector>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/type.h"
+#include "iceberg/util/checked_cast.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+Result<std::shared_ptr<PrimitiveType>> GetPrimitiveType(const BoundTerm& term) {
+  auto primitive = std::dynamic_pointer_cast<PrimitiveType>(term.type());
+  if (primitive == nullptr) {
+    return InvalidExpression("Aggregate requires primitive type, got {}",
+                             term.type()->ToString());
+  }
+  return primitive;
+}
+
+}  // namespace
+
+// -------------------- Bound aggregates --------------------
+
+BoundCountAggregate::BoundCountAggregate(Expression::Operation op, Mode mode,
+                                         std::shared_ptr<BoundTerm> term)
+    : BoundAggregate(op, std::move(term)), mode_(mode) {}
+
+std::string BoundCountAggregate::ToString() const {
+  if (mode_ == Mode::kStar) {
+    return "count(*)";
+  }
+  ICEBERG_DCHECK(term() != nullptr, "Bound count aggregate should have term");
+  switch (mode_) {
+    case Mode::kNull:
+      return std::format("count_null({})", term()->reference()->name());
+    case Mode::kNonNull:
+      return std::format("count({})", term()->reference()->name());
+    case Mode::kStar:
+      break;
+  }
+  std::unreachable();
+}
+
+Result<Literal> BoundCountAggregate::Evaluate(const StructLike& data) const {

Review Comment:
   Thanks for bringing this up.
   
   Between the two options:
   - a single CountAggregate keeps the class count smaller but relies more heavily on branching based on `op()`, which can make the semantics harder to follow;
   - dedicated subclasses more closely match the Java design, improve semantic clarity, and reduce internal conditional logic.
   
   Given that there isn’t a compelling reason to deviate, I’ll align with the Java approach and refactor this into dedicated subclasses for CountNull, CountNotNull and CountStar.
   



-- 
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: issues-unsubscribe@iceberg.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org


Re: [PR] feat: add aggregate expressions and evaluator [iceberg-cpp]

Posted by "Jinchul81 (via GitHub)" <gi...@apache.org>.
Jinchul81 commented on code in PR #335:
URL: https://github.com/apache/iceberg-cpp/pull/335#discussion_r2557106399


##########
src/iceberg/expression/aggregate.cc:
##########
@@ -0,0 +1,289 @@
+/*
+ * 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/aggregate.h"
+
+#include <format>
+#include <optional>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/expression/expression.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/type.h"
+#include "iceberg/util/checked_cast.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+std::string OperationToPrefix(Expression::Operation op) {
+  switch (op) {
+    case Expression::Operation::kMax:
+      return "max";
+    case Expression::Operation::kMin:
+      return "min";
+    case Expression::Operation::kCount:
+    case Expression::Operation::kCountStar:
+      return "count";
+    default:
+      break;
+  }
+  return "aggregate";
+}
+
+Result<std::shared_ptr<PrimitiveType>> GetPrimitiveType(const BoundTerm& term) {
+  auto primitive = std::dynamic_pointer_cast<PrimitiveType>(term.type());
+  if (primitive == nullptr) {
+    return InvalidExpression("Aggregate requires primitive type, got {}",
+                             term.type()->ToString());
+  }
+  return primitive;
+}
+
+}  // namespace
+
+CountAggregate::CountAggregate(Expression::Operation op, Mode mode,
+                               std::shared_ptr<UnboundTerm<BoundReference>> term,
+                               std::shared_ptr<NamedReference> reference)
+    : UnboundAggregate(op),
+      mode_(mode),
+      term_(std::move(term)),
+      reference_(std::move(reference)) {}
+
+Result<std::unique_ptr<CountAggregate>> CountAggregate::Count(
+    std::shared_ptr<UnboundTerm<BoundReference>> term) {
+  auto ref = term->reference();
+  return std::unique_ptr<CountAggregate>(new CountAggregate(
+      Expression::Operation::kCount, Mode::kNonNull, std::move(term), std::move(ref)));
+}
+
+Result<std::unique_ptr<CountAggregate>> CountAggregate::CountNull(
+    std::shared_ptr<UnboundTerm<BoundReference>> term) {
+  auto ref = term->reference();
+  return std::unique_ptr<CountAggregate>(new CountAggregate(
+      Expression::Operation::kCount, Mode::kNull, std::move(term), std::move(ref)));
+}
+
+std::unique_ptr<CountAggregate> CountAggregate::CountStar() {
+  return std::unique_ptr<CountAggregate>(new CountAggregate(
+      Expression::Operation::kCountStar, Mode::kStar, nullptr, nullptr));
+}
+
+std::string CountAggregate::ToString() const {
+  if (mode_ == Mode::kStar) {
+    return "count(*)";
+  }
+  ICEBERG_DCHECK(reference_ != nullptr, "Count aggregate should have reference");
+  switch (mode_) {
+    case Mode::kNull:
+      return std::format("count_null({})", reference_->name());
+    case Mode::kNonNull:
+      return std::format("count({})", reference_->name());
+    case Mode::kStar:
+      break;
+  }
+  std::unreachable();
+}
+
+Result<std::shared_ptr<Expression>> CountAggregate::Bind(const Schema& schema,
+                                                         bool case_sensitive) const {
+  std::shared_ptr<BoundTerm> bound_term;
+  if (term_ != nullptr) {
+    ICEBERG_ASSIGN_OR_THROW(auto bound, term_->Bind(schema, case_sensitive));
+    bound_term = std::move(bound);
+  }
+  auto aggregate =
+      std::make_shared<BoundCountAggregate>(op(), mode_, std::move(bound_term));
+  return aggregate;
+}
+
+BoundCountAggregate::BoundCountAggregate(Expression::Operation op,
+                                         CountAggregate::Mode mode,
+                                         std::shared_ptr<BoundTerm> term)
+    : BoundAggregate(op, std::move(term)), mode_(mode) {}
+
+std::string BoundCountAggregate::ToString() const {
+  if (mode_ == CountAggregate::Mode::kStar) {

Review Comment:
   Please file an issue for the follow up item and add a comment including the reason why you've applied special handling and `TODO(#XYZ)`.



-- 
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: issues-unsubscribe@iceberg.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org


Re: [PR] feat: add aggregate expressions and evaluator [iceberg-cpp]

Posted by "SuKi2cn (via GitHub)" <gi...@apache.org>.
SuKi2cn commented on code in PR #335:
URL: https://github.com/apache/iceberg-cpp/pull/335#discussion_r2557004364


##########
src/iceberg/expression/aggregate.cc:
##########
@@ -0,0 +1,256 @@
+/*
+ * 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/aggregate.h"
+
+#include <format>
+#include <optional>
+#include <vector>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/type.h"
+#include "iceberg/util/checked_cast.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+Result<std::shared_ptr<PrimitiveType>> GetPrimitiveType(const BoundTerm& term) {
+  auto primitive = std::dynamic_pointer_cast<PrimitiveType>(term.type());
+  if (primitive == nullptr) {
+    return InvalidExpression("Aggregate requires primitive type, got {}",
+                             term.type()->ToString());
+  }
+  return primitive;
+}
+
+}  // namespace
+
+// -------------------- Bound aggregates --------------------
+
+BoundCountAggregate::BoundCountAggregate(Expression::Operation op, Mode mode,
+                                         std::shared_ptr<BoundTerm> term)
+    : BoundAggregate(op, std::move(term)), mode_(mode) {}
+
+std::string BoundCountAggregate::ToString() const {
+  if (mode_ == Mode::kStar) {
+    return "count(*)";
+  }
+  ICEBERG_DCHECK(term() != nullptr, "Bound count aggregate should have term");
+  switch (mode_) {
+    case Mode::kNull:
+      return std::format("count_null({})", term()->reference()->name());
+    case Mode::kNonNull:
+      return std::format("count({})", term()->reference()->name());
+    case Mode::kStar:
+      break;
+  }
+  std::unreachable();
+}
+
+Result<Literal> BoundCountAggregate::Evaluate(const StructLike& data) const {
+  switch (mode_) {
+    case Mode::kStar:
+      return Literal::Long(1);
+    case Mode::kNonNull: {
+      ICEBERG_ASSIGN_OR_RAISE(auto literal, term()->Evaluate(data));
+      return Literal::Long(literal.IsNull() ? 0 : 1);
+    }
+    case Mode::kNull: {
+      ICEBERG_ASSIGN_OR_RAISE(auto literal, term()->Evaluate(data));
+      return Literal::Long(literal.IsNull() ? 1 : 0);
+    }
+  }
+  std::unreachable();
+}
+
+BoundValueAggregate::BoundValueAggregate(Expression::Operation op,
+                                         std::shared_ptr<BoundTerm> term)
+    : BoundAggregate(op, std::move(term)) {}
+
+std::string BoundValueAggregate::ToString() const {
+  ICEBERG_DCHECK(term() != nullptr, "Bound value aggregate should have term");
+  auto prefix = op() == Expression::Operation::kMax ? "max" : "min";
+  return std::format("{}({})", prefix, term()->reference()->name());
+}
+
+Result<Literal> BoundValueAggregate::Evaluate(const StructLike& data) const {
+  ICEBERG_ASSIGN_OR_RAISE(auto literal, term()->Evaluate(data));
+  return literal;
+}
+
+// -------------------- Unbound binding --------------------
+
+template <typename B>
+Result<std::shared_ptr<Expression>> UnboundAggregateImpl<B>::Bind(
+    const Schema& schema, bool case_sensitive) const {
+  std::shared_ptr<B> bound_term;
+  if (this->term()) {
+    ICEBERG_ASSIGN_OR_THROW(bound_term, this->term()->Bind(schema, case_sensitive));
+  }
+
+  switch (count_mode_) {
+    case CountMode::kStar:
+    case CountMode::kNull:
+    case CountMode::kNonNull: {
+      auto op = this->op() == Expression::Operation::kCountStar
+                    ? Expression::Operation::kCountStar
+                    : Expression::Operation::kCount;
+      auto mode =
+          count_mode_ == CountMode::kNull
+              ? BoundCountAggregate::Mode::kNull
+              : (count_mode_ == CountMode::kStar ? BoundCountAggregate::Mode::kStar
+                                                 : BoundCountAggregate::Mode::kNonNull);
+      auto aggregate =
+          std::make_shared<BoundCountAggregate>(op, mode, std::move(bound_term));
+      return aggregate;
+    }
+    case CountMode::kNone: {
+      if (this->op() != Expression::Operation::kMax &&
+          this->op() != Expression::Operation::kMin) {
+        return NotSupported("Unsupported aggregate operation");
+      }
+      if (!bound_term) {
+        return InvalidExpression("Aggregate requires a term");
+      }
+      auto aggregate =
+          std::make_shared<BoundValueAggregate>(this->op(), std::move(bound_term));
+      return aggregate;
+    }
+  }
+  std::unreachable();
+}
+
+template class UnboundAggregateImpl<BoundReference>;
+
+// -------------------- AggregateEvaluator --------------------
+
+namespace {
+
+class AggregateEvaluatorImpl : public AggregateEvaluator {

Review Comment:
   Thanks, that’s a good point.



-- 
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: issues-unsubscribe@iceberg.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org


Re: [PR] feat: add aggregate expressions and evaluator [iceberg-cpp]

Posted by "SuKi2cn (via GitHub)" <gi...@apache.org>.
SuKi2cn commented on code in PR #335:
URL: https://github.com/apache/iceberg-cpp/pull/335#discussion_r2564057809


##########
src/iceberg/expression/aggregate.cc:
##########
@@ -0,0 +1,398 @@
+/*
+ * 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/aggregate.h"
+
+#include <format>
+#include <optional>
+#include <vector>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/type.h"
+#include "iceberg/util/checked_cast.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+Result<std::shared_ptr<PrimitiveType>> GetPrimitiveType(const BoundTerm& term) {
+  if (!term.type()->is_primitive()) {
+    return InvalidExpression("Aggregate requires primitive type, got {}",
+                             term.type()->ToString());
+  }
+  return internal::checked_pointer_cast<PrimitiveType>(term.type());
+}
+
+class CountNonNullAggregator : public BoundAggregate::Aggregator {
+ public:
+  explicit CountNonNullAggregator(std::shared_ptr<BoundTerm> term)
+      : term_(std::move(term)) {}
+
+  Status Update(const StructLike& row) override {
+    ICEBERG_ASSIGN_OR_RAISE(auto literal, term_->Evaluate(row));
+    if (!literal.IsNull()) {
+      ++count_;
+    }
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override { return Literal::Long(count_); }
+
+ private:
+  std::shared_ptr<BoundTerm> term_;
+  int64_t count_ = 0;
+};
+
+class CountNullAggregator : public BoundAggregate::Aggregator {
+ public:
+  explicit CountNullAggregator(std::shared_ptr<BoundTerm> term)
+      : term_(std::move(term)) {}
+
+  Status Update(const StructLike& row) override {
+    ICEBERG_ASSIGN_OR_RAISE(auto literal, term_->Evaluate(row));
+    if (literal.IsNull()) {
+      ++count_;
+    }
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override { return Literal::Long(count_); }
+
+ private:
+  std::shared_ptr<BoundTerm> term_;
+  int64_t count_ = 0;
+};
+
+class CountStarAggregator : public BoundAggregate::Aggregator {
+ public:
+  Status Update(const StructLike& /*row*/) override {
+    ++count_;
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override { return Literal::Long(count_); }
+
+ private:
+  int64_t count_ = 0;
+};
+
+class ValueAggregatorImpl : public BoundAggregate::Aggregator {
+ public:
+  ValueAggregatorImpl(bool is_max, std::shared_ptr<BoundTerm> term)
+      : is_max_(is_max), term_(std::move(term)) {}
+
+  Status Update(const StructLike& row) override {
+    ICEBERG_ASSIGN_OR_RAISE(auto val_literal, term_->Evaluate(row));
+    if (val_literal.IsNull()) {
+      return {};
+    }
+    if (!current_) {
+      current_ = std::move(val_literal);
+      return {};
+    }
+
+    auto ordering = val_literal <=> *current_;
+    if (ordering == std::partial_ordering::unordered) {
+      return InvalidExpression("Cannot compare literals of type {}",
+                               val_literal.type()->ToString());
+    }
+
+    if (is_max_) {
+      if (ordering == std::partial_ordering::greater) {
+        current_ = std::move(val_literal);
+      }
+    } else {
+      if (ordering == std::partial_ordering::less) {
+        current_ = std::move(val_literal);
+      }
+    }
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override {
+    if (current_) {
+      return *current_;
+    }
+    ICEBERG_ASSIGN_OR_RAISE(auto type, GetPrimitiveType(*term_));
+    return Literal::Null(type);
+  }
+
+ private:
+  bool is_max_;
+  std::shared_ptr<BoundTerm> term_;
+  std::optional<Literal> current_;
+};
+
+}  // namespace
+
+// -------------------- Bound aggregates --------------------
+
+CountNonNullAggregate::CountNonNullAggregate(std::shared_ptr<BoundTerm> term)
+    : CountAggregate(Expression::Operation::kCount, std::move(term)) {}
+
+Result<std::shared_ptr<CountNonNullAggregate>> CountNonNullAggregate::Make(
+    std::shared_ptr<BoundTerm> term) {
+  if (!term) {
+    return InvalidExpression("Bound count aggregate requires non-null term");
+  }
+  return std::shared_ptr<CountNonNullAggregate>(
+      new CountNonNullAggregate(std::move(term)));
+}
+
+std::string CountNonNullAggregate::ToString() const {
+  ICEBERG_DCHECK(term() != nullptr, "Bound count aggregate should have term");
+  return std::format("count({})", term()->reference()->name());
+}
+
+Result<Literal> CountNonNullAggregate::Evaluate(const StructLike& data) const {
+  ICEBERG_ASSIGN_OR_RAISE(auto literal, term()->Evaluate(data));
+  return Literal::Long(literal.IsNull() ? 0 : 1);
+}
+
+Result<std::unique_ptr<BoundAggregate::Aggregator>> CountNonNullAggregate::NewAggregator()
+    const {
+  return std::unique_ptr<BoundAggregate::Aggregator>(new CountNonNullAggregator(term()));
+}
+
+CountNullAggregate::CountNullAggregate(std::shared_ptr<BoundTerm> term)
+    : CountAggregate(Expression::Operation::kCountNull, std::move(term)) {}
+
+Result<std::shared_ptr<CountNullAggregate>> CountNullAggregate::Make(
+    std::shared_ptr<BoundTerm> term) {
+  if (!term) {
+    return InvalidExpression("Bound count aggregate requires non-null term");
+  }
+  return std::shared_ptr<CountNullAggregate>(new CountNullAggregate(std::move(term)));
+}
+
+std::string CountNullAggregate::ToString() const {
+  ICEBERG_DCHECK(term() != nullptr, "Bound count aggregate should have term");
+  return std::format("count_null({})", term()->reference()->name());
+}
+
+Result<Literal> CountNullAggregate::Evaluate(const StructLike& data) const {
+  ICEBERG_ASSIGN_OR_RAISE(auto literal, term()->Evaluate(data));
+  return Literal::Long(literal.IsNull() ? 1 : 0);
+}
+
+Result<std::unique_ptr<BoundAggregate::Aggregator>> CountNullAggregate::NewAggregator()
+    const {
+  return std::unique_ptr<BoundAggregate::Aggregator>(new CountNullAggregator(term()));
+}
+
+CountStarAggregate::CountStarAggregate()
+    : CountAggregate(Expression::Operation::kCountStar, nullptr) {}
+
+Result<std::shared_ptr<CountStarAggregate>> CountStarAggregate::Make() {
+  return std::shared_ptr<CountStarAggregate>(new CountStarAggregate());
+}
+
+std::string CountStarAggregate::ToString() const { return "count(*)"; }
+
+Result<Literal> CountStarAggregate::Evaluate(const StructLike& data) const {
+  return Literal::Long(1);
+}
+
+Result<std::unique_ptr<BoundAggregate::Aggregator>> CountStarAggregate::NewAggregator()
+    const {
+  return std::unique_ptr<BoundAggregate::Aggregator>(new CountStarAggregator());
+}
+
+ValueAggregate::ValueAggregate(Expression::Operation op, std::shared_ptr<BoundTerm> term)
+    : BoundAggregate(op, std::move(term)) {}
+
+std::string ValueAggregate::ToString() const {
+  ICEBERG_DCHECK(term() != nullptr, "Bound value aggregate should have term");
+  auto prefix = op() == Expression::Operation::kMax ? "max" : "min";
+  return std::format("{}({})", prefix, term()->reference()->name());
+}
+
+Result<Literal> ValueAggregate::Evaluate(const StructLike& data) const {
+  ICEBERG_ASSIGN_OR_RAISE(auto literal, term()->Evaluate(data));
+  return literal;
+}
+
+Result<std::unique_ptr<BoundAggregate::Aggregator>> ValueAggregate::NewAggregator()
+    const {
+  bool is_max = op() == Expression::Operation::kMax;
+  return std::unique_ptr<BoundAggregate::Aggregator>(
+      new ValueAggregatorImpl(is_max, term()));
+}
+
+// -------------------- Unbound binding --------------------
+
+template <typename B>
+Result<std::shared_ptr<Expression>> UnboundAggregateImpl<B>::Bind(
+    const Schema& schema, bool case_sensitive) const {
+  ICEBERG_DCHECK(UnboundAggregateImpl<B>::IsSupportedOp(this->op()),
+                 "Unexpected aggregate operation");
+
+  std::shared_ptr<B> bound_term;
+  if (this->term()) {
+    ICEBERG_ASSIGN_OR_THROW(bound_term, this->term()->Bind(schema, case_sensitive));
+  }
+
+  switch (this->op()) {
+    case Expression::Operation::kCountStar: {
+      ICEBERG_ASSIGN_OR_THROW(auto aggregate, CountStarAggregate::Make());
+      return aggregate;
+    }
+    case Expression::Operation::kCount: {
+      if (!bound_term) {
+        return InvalidExpression("Aggregate requires a term");
+      }
+      ICEBERG_ASSIGN_OR_THROW(auto aggregate,
+                              CountNonNullAggregate::Make(std::move(bound_term)));
+      return aggregate;
+    }
+    case Expression::Operation::kCountNull: {
+      if (!bound_term) {
+        return InvalidExpression("Aggregate requires a term");
+      }
+      ICEBERG_ASSIGN_OR_THROW(auto aggregate,
+                              CountNullAggregate::Make(std::move(bound_term)));
+      return aggregate;
+    }
+    case Expression::Operation::kMax:
+    case Expression::Operation::kMin: {
+      if (!bound_term) {
+        return InvalidExpression("Aggregate requires a term");
+      }
+      auto aggregate =
+          std::make_shared<ValueAggregate>(this->op(), std::move(bound_term));
+      return aggregate;
+    }
+    default:
+      return NotSupported("Unsupported aggregate operation");
+  }
+}
+
+template <typename B>
+Result<std::shared_ptr<UnboundAggregateImpl<B>>> UnboundAggregateImpl<B>::Make(
+    Expression::Operation op, std::shared_ptr<UnboundTerm<B>> term) {
+  if (!IsSupportedOp(op)) {
+    return NotSupported("Unsupported aggregate operation: {}", ::iceberg::ToString(op));
+  }
+  if (op != Expression::Operation::kCountStar && !term) {
+    return InvalidExpression("Aggregate term cannot be null unless COUNT(*)");
+  }
+
+  return std::shared_ptr<UnboundAggregateImpl<B>>(
+      new UnboundAggregateImpl<B>(op, std::move(term)));
+}
+
+template <typename B>
+std::string UnboundAggregateImpl<B>::ToString() const {

Review Comment:
   > Can we move this to `Aggregate::ToString()` so all other overrides can be removed.
   
   Thanks, that makes sense. I’ve moved the common `ToString()` implementation into Aggregate so all aggregates share the same formatting logic, and removed the redundant overrides from the concrete classes.



-- 
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: issues-unsubscribe@iceberg.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org


Re: [PR] feat: add aggregate expressions and evaluator [iceberg-cpp]

Posted by "SuKi2cn (via GitHub)" <gi...@apache.org>.
SuKi2cn commented on code in PR #335:
URL: https://github.com/apache/iceberg-cpp/pull/335#discussion_r2564089486


##########
src/iceberg/expression/aggregate.h:
##########
@@ -0,0 +1,230 @@
+/*
+ * 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/aggregate.h
+/// Aggregate expression definitions.
+
+#include <memory>
+#include <optional>
+#include <string>
+#include <vector>
+
+#include "iceberg/expression/expression.h"
+#include "iceberg/expression/term.h"
+#include "iceberg/result.h"
+#include "iceberg/type_fwd.h"
+
+namespace iceberg {
+
+/// \brief Base aggregate holding an operation and a term.
+template <TermType T>
+class ICEBERG_EXPORT Aggregate : public virtual Expression {
+ public:
+  ~Aggregate() override = default;
+
+  Expression::Operation op() const override { return operation_; }
+
+  const std::shared_ptr<T>& term() const { return term_; }
+
+ protected:
+  Aggregate(Expression::Operation op, std::shared_ptr<T> term)
+      : operation_(op), term_(std::move(term)) {}
+
+  Expression::Operation operation_;
+  std::shared_ptr<T> term_;
+};
+
+/// \brief Base class for unbound aggregates.
+class ICEBERG_EXPORT UnboundAggregate : public virtual Expression,
+                                        public Unbound<Expression> {
+ public:
+  ~UnboundAggregate() override = default;
+
+  bool is_unbound_aggregate() const override { return true; }
+};
+
+/// \brief Template for unbound aggregates that carry a term and operation.
+template <typename B>
+class ICEBERG_EXPORT UnboundAggregateImpl : public UnboundAggregate,
+                                            public Aggregate<UnboundTerm<B>> {
+  using BASE = Aggregate<UnboundTerm<B>>;
+
+ public:
+  static Result<std::shared_ptr<UnboundAggregateImpl<B>>> Make(
+      Expression::Operation op, std::shared_ptr<UnboundTerm<B>> term);
+
+  std::shared_ptr<NamedReference> reference() override {
+    return BASE::term() ? BASE::term()->reference() : nullptr;
+  }
+
+  Result<std::shared_ptr<Expression>> Bind(const Schema& schema,
+                                           bool case_sensitive) const override;
+
+  std::string ToString() const override;
+
+ private:
+  static constexpr bool IsSupportedOp(Expression::Operation op) {
+    return op == Expression::Operation::kCount ||
+           op == Expression::Operation::kCountNull ||
+           op == Expression::Operation::kCountStar || op == Expression::Operation::kMax ||
+           op == Expression::Operation::kMin;
+  }
+
+  UnboundAggregateImpl(Expression::Operation op, std::shared_ptr<UnboundTerm<B>> term)
+      : BASE(op, std::move(term)) {
+    ICEBERG_DCHECK(IsSupportedOp(op), "Unexpected aggregate operation");
+    ICEBERG_DCHECK(op == Expression::Operation::kCountStar || BASE::term() != nullptr,
+                   "Aggregate term cannot be null unless COUNT(*)");
+  }
+};
+
+/// \brief Base class for bound aggregates.
+class ICEBERG_EXPORT BoundAggregate : public Aggregate<BoundTerm>, public Bound {
+ public:
+  using Aggregate<BoundTerm>::op;
+  using Aggregate<BoundTerm>::term;
+
+  class ICEBERG_EXPORT Aggregator {
+   public:
+    virtual ~Aggregator() = default;
+
+    virtual Status Update(const StructLike& row) = 0;
+    virtual Status Update(const DataFile& file) {
+      return NotSupported("Aggregating DataFile not supported");
+    }
+    virtual Result<Literal> ResultLiteral() const = 0;
+  };
+
+  std::shared_ptr<BoundReference> reference() override {
+    ICEBERG_DCHECK(term_ != nullptr || op() == Expression::Operation::kCountStar,
+                   "Bound aggregate term should not be null unless COUNT(*)");
+    return term_ ? term_->reference() : nullptr;
+  }
+
+  Result<Literal> Evaluate(const StructLike& data) const override = 0;
+
+  bool is_bound_aggregate() const override { return true; }
+
+  enum class Kind : int8_t {
+    // Count aggregates (COUNT, COUNT_STAR, COUNT_NULL)
+    kCount = 0,
+    // Value aggregates (MIN, MAX)
+    kValue,
+  };
+
+  virtual Kind kind() const = 0;
+  virtual Result<std::unique_ptr<Aggregator>> NewAggregator() const = 0;

Review Comment:
   > If it will not return any error, we can use `std::unique_ptr<Aggregator>` as its return type.
   
   Agreed. Since `NewAggregator` cannot fail, I’ve switched its return type to `std::unique_ptr<Aggregator>` and removed the `Result` wrapper.



-- 
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: issues-unsubscribe@iceberg.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org


Re: [PR] feat: add aggregate expressions and evaluator [iceberg-cpp]

Posted by "Jinchul81 (via GitHub)" <gi...@apache.org>.
Jinchul81 commented on code in PR #335:
URL: https://github.com/apache/iceberg-cpp/pull/335#discussion_r2553974806


##########
src/iceberg/expression/aggregate.cc:
##########
@@ -0,0 +1,289 @@
+/*
+ * 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/aggregate.h"
+
+#include <format>
+#include <optional>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/expression/expression.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/type.h"
+#include "iceberg/util/checked_cast.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+std::string OperationToPrefix(Expression::Operation op) {
+  switch (op) {
+    case Expression::Operation::kMax:
+      return "max";
+    case Expression::Operation::kMin:
+      return "min";
+    case Expression::Operation::kCount:
+    case Expression::Operation::kCountStar:
+      return "count";
+    default:
+      break;
+  }
+  return "aggregate";
+}
+
+Result<std::shared_ptr<PrimitiveType>> GetPrimitiveType(const BoundTerm& term) {
+  auto primitive = std::dynamic_pointer_cast<PrimitiveType>(term.type());
+  if (primitive == nullptr) {
+    return InvalidExpression("Aggregate requires primitive type, got {}",
+                             term.type()->ToString());
+  }
+  return primitive;
+}
+
+}  // namespace
+
+CountAggregate::CountAggregate(Expression::Operation op, Mode mode,
+                               std::shared_ptr<UnboundTerm<BoundReference>> term,
+                               std::shared_ptr<NamedReference> reference)
+    : UnboundAggregate(op),
+      mode_(mode),
+      term_(std::move(term)),
+      reference_(std::move(reference)) {}
+
+Result<std::unique_ptr<CountAggregate>> CountAggregate::Count(
+    std::shared_ptr<UnboundTerm<BoundReference>> term) {
+  auto ref = term->reference();
+  return std::unique_ptr<CountAggregate>(new CountAggregate(
+      Expression::Operation::kCount, Mode::kNonNull, std::move(term), std::move(ref)));
+}
+
+Result<std::unique_ptr<CountAggregate>> CountAggregate::CountNull(
+    std::shared_ptr<UnboundTerm<BoundReference>> term) {
+  auto ref = term->reference();
+  return std::unique_ptr<CountAggregate>(new CountAggregate(
+      Expression::Operation::kCount, Mode::kNull, std::move(term), std::move(ref)));
+}
+
+std::unique_ptr<CountAggregate> CountAggregate::CountStar() {
+  return std::unique_ptr<CountAggregate>(new CountAggregate(
+      Expression::Operation::kCountStar, Mode::kStar, nullptr, nullptr));
+}
+
+std::string CountAggregate::ToString() const {
+  if (mode_ == Mode::kStar) {
+    return "count(*)";
+  }
+  ICEBERG_DCHECK(reference_ != nullptr, "Count aggregate should have reference");
+  switch (mode_) {
+    case Mode::kNull:
+      return std::format("count_null({})", reference_->name());
+    case Mode::kNonNull:
+      return std::format("count({})", reference_->name());
+    case Mode::kStar:
+      break;
+  }
+  std::unreachable();
+}
+
+Result<std::shared_ptr<Expression>> CountAggregate::Bind(const Schema& schema,
+                                                         bool case_sensitive) const {
+  std::shared_ptr<BoundTerm> bound_term;
+  if (term_ != nullptr) {
+    ICEBERG_ASSIGN_OR_THROW(auto bound, term_->Bind(schema, case_sensitive));
+    bound_term = std::move(bound);
+  }
+  auto aggregate =
+      std::make_shared<BoundCountAggregate>(op(), mode_, std::move(bound_term));
+  return aggregate;
+}
+
+BoundCountAggregate::BoundCountAggregate(Expression::Operation op,
+                                         CountAggregate::Mode mode,
+                                         std::shared_ptr<BoundTerm> term)
+    : BoundAggregate(op, std::move(term)), mode_(mode) {}
+
+std::string BoundCountAggregate::ToString() const {
+  if (mode_ == CountAggregate::Mode::kStar) {

Review Comment:
   Let's set aside the idea of copying the Java implementation. Can you please clarify why we cannot make it as the generic approach?



-- 
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: issues-unsubscribe@iceberg.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org


Re: [PR] feat: add aggregate expressions and evaluator [iceberg-cpp]

Posted by "SuKi2cn (via GitHub)" <gi...@apache.org>.
SuKi2cn commented on code in PR #335:
URL: https://github.com/apache/iceberg-cpp/pull/335#discussion_r2563849082


##########
src/iceberg/expression/aggregate.cc:
##########
@@ -0,0 +1,398 @@
+/*
+ * 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/aggregate.h"
+
+#include <format>
+#include <optional>
+#include <vector>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/type.h"
+#include "iceberg/util/checked_cast.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+Result<std::shared_ptr<PrimitiveType>> GetPrimitiveType(const BoundTerm& term) {
+  if (!term.type()->is_primitive()) {
+    return InvalidExpression("Aggregate requires primitive type, got {}",
+                             term.type()->ToString());
+  }
+  return internal::checked_pointer_cast<PrimitiveType>(term.type());
+}
+
+class CountNonNullAggregator : public BoundAggregate::Aggregator {
+ public:
+  explicit CountNonNullAggregator(std::shared_ptr<BoundTerm> term)
+      : term_(std::move(term)) {}
+
+  Status Update(const StructLike& row) override {
+    ICEBERG_ASSIGN_OR_RAISE(auto literal, term_->Evaluate(row));
+    if (!literal.IsNull()) {
+      ++count_;
+    }
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override { return Literal::Long(count_); }

Review Comment:
   > Do we actually need to use `Result` to wrap the return type?
   You are right. I’ll update the interface and implementations to return `Literal` directly instead.
   
   



##########
src/iceberg/expression/aggregate.cc:
##########
@@ -0,0 +1,398 @@
+/*
+ * 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/aggregate.h"
+
+#include <format>
+#include <optional>
+#include <vector>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/type.h"
+#include "iceberg/util/checked_cast.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+Result<std::shared_ptr<PrimitiveType>> GetPrimitiveType(const BoundTerm& term) {
+  if (!term.type()->is_primitive()) {
+    return InvalidExpression("Aggregate requires primitive type, got {}",
+                             term.type()->ToString());
+  }
+  return internal::checked_pointer_cast<PrimitiveType>(term.type());
+}
+
+class CountNonNullAggregator : public BoundAggregate::Aggregator {
+ public:
+  explicit CountNonNullAggregator(std::shared_ptr<BoundTerm> term)
+      : term_(std::move(term)) {}
+
+  Status Update(const StructLike& row) override {
+    ICEBERG_ASSIGN_OR_RAISE(auto literal, term_->Evaluate(row));
+    if (!literal.IsNull()) {
+      ++count_;
+    }
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override { return Literal::Long(count_); }

Review Comment:
   > Do we actually need to use `Result` to wrap the return type?
   
   You are right. I’ll update the interface and implementations to return `Literal` directly instead.
   
   



-- 
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: issues-unsubscribe@iceberg.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org


Re: [PR] feat: add aggregate expressions and evaluator [iceberg-cpp]

Posted by "SuKi2cn (via GitHub)" <gi...@apache.org>.
SuKi2cn commented on code in PR #335:
URL: https://github.com/apache/iceberg-cpp/pull/335#discussion_r2564021847


##########
src/iceberg/expression/aggregate.cc:
##########
@@ -0,0 +1,398 @@
+/*
+ * 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/aggregate.h"
+
+#include <format>
+#include <optional>
+#include <vector>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/type.h"
+#include "iceberg/util/checked_cast.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+Result<std::shared_ptr<PrimitiveType>> GetPrimitiveType(const BoundTerm& term) {
+  if (!term.type()->is_primitive()) {
+    return InvalidExpression("Aggregate requires primitive type, got {}",
+                             term.type()->ToString());
+  }
+  return internal::checked_pointer_cast<PrimitiveType>(term.type());
+}
+
+class CountNonNullAggregator : public BoundAggregate::Aggregator {
+ public:
+  explicit CountNonNullAggregator(std::shared_ptr<BoundTerm> term)
+      : term_(std::move(term)) {}
+
+  Status Update(const StructLike& row) override {
+    ICEBERG_ASSIGN_OR_RAISE(auto literal, term_->Evaluate(row));
+    if (!literal.IsNull()) {
+      ++count_;
+    }
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override { return Literal::Long(count_); }
+
+ private:
+  std::shared_ptr<BoundTerm> term_;
+  int64_t count_ = 0;
+};
+
+class CountNullAggregator : public BoundAggregate::Aggregator {
+ public:
+  explicit CountNullAggregator(std::shared_ptr<BoundTerm> term)
+      : term_(std::move(term)) {}
+
+  Status Update(const StructLike& row) override {
+    ICEBERG_ASSIGN_OR_RAISE(auto literal, term_->Evaluate(row));
+    if (literal.IsNull()) {
+      ++count_;
+    }
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override { return Literal::Long(count_); }
+
+ private:
+  std::shared_ptr<BoundTerm> term_;
+  int64_t count_ = 0;
+};
+
+class CountStarAggregator : public BoundAggregate::Aggregator {
+ public:
+  Status Update(const StructLike& /*row*/) override {
+    ++count_;
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override { return Literal::Long(count_); }
+
+ private:
+  int64_t count_ = 0;
+};
+
+class ValueAggregatorImpl : public BoundAggregate::Aggregator {
+ public:
+  ValueAggregatorImpl(bool is_max, std::shared_ptr<BoundTerm> term)
+      : is_max_(is_max), term_(std::move(term)) {}
+
+  Status Update(const StructLike& row) override {
+    ICEBERG_ASSIGN_OR_RAISE(auto val_literal, term_->Evaluate(row));
+    if (val_literal.IsNull()) {
+      return {};
+    }
+    if (!current_) {
+      current_ = std::move(val_literal);
+      return {};
+    }
+
+    auto ordering = val_literal <=> *current_;
+    if (ordering == std::partial_ordering::unordered) {
+      return InvalidExpression("Cannot compare literals of type {}",
+                               val_literal.type()->ToString());
+    }
+
+    if (is_max_) {
+      if (ordering == std::partial_ordering::greater) {
+        current_ = std::move(val_literal);
+      }
+    } else {
+      if (ordering == std::partial_ordering::less) {
+        current_ = std::move(val_literal);
+      }
+    }
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override {
+    if (current_) {
+      return *current_;
+    }
+    ICEBERG_ASSIGN_OR_RAISE(auto type, GetPrimitiveType(*term_));
+    return Literal::Null(type);
+  }
+
+ private:
+  bool is_max_;
+  std::shared_ptr<BoundTerm> term_;
+  std::optional<Literal> current_;
+};
+
+}  // namespace
+
+// -------------------- Bound aggregates --------------------
+
+CountNonNullAggregate::CountNonNullAggregate(std::shared_ptr<BoundTerm> term)
+    : CountAggregate(Expression::Operation::kCount, std::move(term)) {}
+
+Result<std::shared_ptr<CountNonNullAggregate>> CountNonNullAggregate::Make(
+    std::shared_ptr<BoundTerm> term) {
+  if (!term) {
+    return InvalidExpression("Bound count aggregate requires non-null term");
+  }
+  return std::shared_ptr<CountNonNullAggregate>(
+      new CountNonNullAggregate(std::move(term)));
+}
+
+std::string CountNonNullAggregate::ToString() const {
+  ICEBERG_DCHECK(term() != nullptr, "Bound count aggregate should have term");
+  return std::format("count({})", term()->reference()->name());
+}
+
+Result<Literal> CountNonNullAggregate::Evaluate(const StructLike& data) const {
+  ICEBERG_ASSIGN_OR_RAISE(auto literal, term()->Evaluate(data));
+  return Literal::Long(literal.IsNull() ? 0 : 1);
+}
+
+Result<std::unique_ptr<BoundAggregate::Aggregator>> CountNonNullAggregate::NewAggregator()
+    const {
+  return std::unique_ptr<BoundAggregate::Aggregator>(new CountNonNullAggregator(term()));
+}
+
+CountNullAggregate::CountNullAggregate(std::shared_ptr<BoundTerm> term)
+    : CountAggregate(Expression::Operation::kCountNull, std::move(term)) {}
+
+Result<std::shared_ptr<CountNullAggregate>> CountNullAggregate::Make(
+    std::shared_ptr<BoundTerm> term) {
+  if (!term) {
+    return InvalidExpression("Bound count aggregate requires non-null term");
+  }
+  return std::shared_ptr<CountNullAggregate>(new CountNullAggregate(std::move(term)));
+}
+
+std::string CountNullAggregate::ToString() const {
+  ICEBERG_DCHECK(term() != nullptr, "Bound count aggregate should have term");
+  return std::format("count_null({})", term()->reference()->name());
+}
+
+Result<Literal> CountNullAggregate::Evaluate(const StructLike& data) const {
+  ICEBERG_ASSIGN_OR_RAISE(auto literal, term()->Evaluate(data));
+  return Literal::Long(literal.IsNull() ? 1 : 0);
+}
+
+Result<std::unique_ptr<BoundAggregate::Aggregator>> CountNullAggregate::NewAggregator()
+    const {
+  return std::unique_ptr<BoundAggregate::Aggregator>(new CountNullAggregator(term()));
+}
+
+CountStarAggregate::CountStarAggregate()
+    : CountAggregate(Expression::Operation::kCountStar, nullptr) {}
+
+Result<std::shared_ptr<CountStarAggregate>> CountStarAggregate::Make() {
+  return std::shared_ptr<CountStarAggregate>(new CountStarAggregate());
+}
+
+std::string CountStarAggregate::ToString() const { return "count(*)"; }
+
+Result<Literal> CountStarAggregate::Evaluate(const StructLike& data) const {
+  return Literal::Long(1);
+}
+
+Result<std::unique_ptr<BoundAggregate::Aggregator>> CountStarAggregate::NewAggregator()
+    const {
+  return std::unique_ptr<BoundAggregate::Aggregator>(new CountStarAggregator());
+}
+
+ValueAggregate::ValueAggregate(Expression::Operation op, std::shared_ptr<BoundTerm> term)
+    : BoundAggregate(op, std::move(term)) {}
+
+std::string ValueAggregate::ToString() const {
+  ICEBERG_DCHECK(term() != nullptr, "Bound value aggregate should have term");
+  auto prefix = op() == Expression::Operation::kMax ? "max" : "min";
+  return std::format("{}({})", prefix, term()->reference()->name());
+}
+
+Result<Literal> ValueAggregate::Evaluate(const StructLike& data) const {
+  ICEBERG_ASSIGN_OR_RAISE(auto literal, term()->Evaluate(data));
+  return literal;
+}
+
+Result<std::unique_ptr<BoundAggregate::Aggregator>> ValueAggregate::NewAggregator()
+    const {
+  bool is_max = op() == Expression::Operation::kMax;
+  return std::unique_ptr<BoundAggregate::Aggregator>(
+      new ValueAggregatorImpl(is_max, term()));
+}
+
+// -------------------- Unbound binding --------------------
+
+template <typename B>
+Result<std::shared_ptr<Expression>> UnboundAggregateImpl<B>::Bind(
+    const Schema& schema, bool case_sensitive) const {
+  ICEBERG_DCHECK(UnboundAggregateImpl<B>::IsSupportedOp(this->op()),
+                 "Unexpected aggregate operation");
+
+  std::shared_ptr<B> bound_term;
+  if (this->term()) {
+    ICEBERG_ASSIGN_OR_THROW(bound_term, this->term()->Bind(schema, case_sensitive));
+  }
+
+  switch (this->op()) {
+    case Expression::Operation::kCountStar: {
+      ICEBERG_ASSIGN_OR_THROW(auto aggregate, CountStarAggregate::Make());
+      return aggregate;
+    }

Review Comment:
   > Same for below.
   
   Since the `Make` now return `Result<std::unique_ptr<...>>`, I used `transform` here to convert the result to `std::shared_ptr<Expression>`, keeping ownership single until binding.
   



-- 
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: issues-unsubscribe@iceberg.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org


Re: [PR] feat: add aggregate expressions and evaluator [iceberg-cpp]

Posted by "SuKi2cn (via GitHub)" <gi...@apache.org>.
SuKi2cn commented on code in PR #335:
URL: https://github.com/apache/iceberg-cpp/pull/335#discussion_r2549057584


##########
src/iceberg/expression/aggregate.h:
##########
@@ -0,0 +1,186 @@
+/*
+ * 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/aggregate.h
+/// Aggregate expression definitions.
+
+#include <memory>
+
+#include "iceberg/expression/expression.h"
+#include "iceberg/expression/term.h"
+#include "iceberg/result.h"
+
+namespace iceberg {
+
+class AggregateEvaluator;
+
+/// \brief Base class for aggregate expressions.
+class ICEBERG_EXPORT Aggregate : public Expression {
+ public:
+  ~Aggregate() override = default;
+
+  Expression::Operation op() const override { return operation_; }
+
+  bool is_unbound_aggregate() const override { return false; }
+  bool is_bound_aggregate() const override { return false; }
+
+ protected:
+  explicit Aggregate(Expression::Operation op) : operation_(op) {}
+
+ private:
+  Expression::Operation operation_;
+};
+
+/// \brief Unbound aggregate with an optional term.
+class ICEBERG_EXPORT UnboundAggregate : public Aggregate, public Unbound<Expression> {
+ public:
+  ~UnboundAggregate() override = default;
+
+  bool is_unbound_aggregate() const override { return true; }
+
+  /// \brief Returns the unbound reference if the aggregate has a term.
+  std::shared_ptr<NamedReference> reference() override = 0;
+
+ protected:
+  explicit UnboundAggregate(Expression::Operation op) : Aggregate(op) {}
+};
+
+/// \brief Bound aggregate with an optional term.
+class ICEBERG_EXPORT BoundAggregate : public Aggregate {
+ public:
+  ~BoundAggregate() override = default;
+
+  bool is_bound_aggregate() const override { return true; }
+
+  const std::shared_ptr<BoundTerm>& term() const { return term_; }
+
+ protected:
+  BoundAggregate(Expression::Operation op, std::shared_ptr<BoundTerm> term)
+      : Aggregate(op), term_(std::move(term)) {}
+
+ private:
+  std::shared_ptr<BoundTerm> term_;
+};
+
+/// \brief COUNT aggregate variants.
+class ICEBERG_EXPORT CountAggregate : public UnboundAggregate {

Review Comment:
   Gotcha! In progress.



-- 
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: issues-unsubscribe@iceberg.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org


Re: [PR] feat: add aggregate expressions and evaluator [iceberg-cpp]

Posted by "Jinchul81 (via GitHub)" <gi...@apache.org>.
Jinchul81 commented on code in PR #335:
URL: https://github.com/apache/iceberg-cpp/pull/335#discussion_r2553995325


##########
src/iceberg/test/aggregate_test.cc:
##########
@@ -0,0 +1,132 @@
+/*
+ * 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/aggregate.h"
+
+#include <gtest/gtest.h>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/expression/expressions.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/schema.h"
+#include "iceberg/test/matchers.h"
+#include "iceberg/type.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+class VectorStructLike : public StructLike {
+ public:
+  explicit VectorStructLike(std::vector<Scalar> fields) : fields_(std::move(fields)) {}
+
+  Result<Scalar> GetField(size_t pos) const override {
+    if (pos >= fields_.size()) {

Review Comment:
   Please write the code like `if (pos >= fields_.size()) [[unlikely]] {` to help branch prediction.
   



-- 
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: issues-unsubscribe@iceberg.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org


Re: [PR] feat: add aggregate expressions and evaluator [iceberg-cpp]

Posted by "SuKi2cn (via GitHub)" <gi...@apache.org>.
SuKi2cn commented on code in PR #335:
URL: https://github.com/apache/iceberg-cpp/pull/335#discussion_r2564108005


##########
src/iceberg/expression/aggregate.h:
##########
@@ -0,0 +1,230 @@
+/*
+ * 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/aggregate.h
+/// Aggregate expression definitions.
+
+#include <memory>
+#include <optional>
+#include <string>
+#include <vector>
+
+#include "iceberg/expression/expression.h"
+#include "iceberg/expression/term.h"
+#include "iceberg/result.h"
+#include "iceberg/type_fwd.h"
+
+namespace iceberg {
+
+/// \brief Base aggregate holding an operation and a term.
+template <TermType T>
+class ICEBERG_EXPORT Aggregate : public virtual Expression {
+ public:
+  ~Aggregate() override = default;
+
+  Expression::Operation op() const override { return operation_; }
+
+  const std::shared_ptr<T>& term() const { return term_; }
+
+ protected:
+  Aggregate(Expression::Operation op, std::shared_ptr<T> term)
+      : operation_(op), term_(std::move(term)) {}
+
+  Expression::Operation operation_;
+  std::shared_ptr<T> term_;
+};
+
+/// \brief Base class for unbound aggregates.
+class ICEBERG_EXPORT UnboundAggregate : public virtual Expression,
+                                        public Unbound<Expression> {
+ public:
+  ~UnboundAggregate() override = default;
+
+  bool is_unbound_aggregate() const override { return true; }
+};
+
+/// \brief Template for unbound aggregates that carry a term and operation.
+template <typename B>
+class ICEBERG_EXPORT UnboundAggregateImpl : public UnboundAggregate,
+                                            public Aggregate<UnboundTerm<B>> {
+  using BASE = Aggregate<UnboundTerm<B>>;
+
+ public:
+  static Result<std::shared_ptr<UnboundAggregateImpl<B>>> Make(
+      Expression::Operation op, std::shared_ptr<UnboundTerm<B>> term);
+
+  std::shared_ptr<NamedReference> reference() override {
+    return BASE::term() ? BASE::term()->reference() : nullptr;
+  }
+
+  Result<std::shared_ptr<Expression>> Bind(const Schema& schema,
+                                           bool case_sensitive) const override;
+
+  std::string ToString() const override;
+
+ private:
+  static constexpr bool IsSupportedOp(Expression::Operation op) {
+    return op == Expression::Operation::kCount ||
+           op == Expression::Operation::kCountNull ||
+           op == Expression::Operation::kCountStar || op == Expression::Operation::kMax ||
+           op == Expression::Operation::kMin;
+  }
+
+  UnboundAggregateImpl(Expression::Operation op, std::shared_ptr<UnboundTerm<B>> term)
+      : BASE(op, std::move(term)) {
+    ICEBERG_DCHECK(IsSupportedOp(op), "Unexpected aggregate operation");
+    ICEBERG_DCHECK(op == Expression::Operation::kCountStar || BASE::term() != nullptr,
+                   "Aggregate term cannot be null unless COUNT(*)");
+  }
+};
+
+/// \brief Base class for bound aggregates.
+class ICEBERG_EXPORT BoundAggregate : public Aggregate<BoundTerm>, public Bound {
+ public:
+  using Aggregate<BoundTerm>::op;
+  using Aggregate<BoundTerm>::term;
+
+  class ICEBERG_EXPORT Aggregator {
+   public:
+    virtual ~Aggregator() = default;
+
+    virtual Status Update(const StructLike& row) = 0;
+    virtual Status Update(const DataFile& file) {
+      return NotSupported("Aggregating DataFile not supported");
+    }
+    virtual Result<Literal> ResultLiteral() const = 0;
+  };
+
+  std::shared_ptr<BoundReference> reference() override {
+    ICEBERG_DCHECK(term_ != nullptr || op() == Expression::Operation::kCountStar,
+                   "Bound aggregate term should not be null unless COUNT(*)");
+    return term_ ? term_->reference() : nullptr;
+  }
+
+  Result<Literal> Evaluate(const StructLike& data) const override = 0;
+
+  bool is_bound_aggregate() const override { return true; }
+
+  enum class Kind : int8_t {
+    // Count aggregates (COUNT, COUNT_STAR, COUNT_NULL)
+    kCount = 0,
+    // Value aggregates (MIN, MAX)
+    kValue,
+  };
+
+  virtual Kind kind() const = 0;
+  virtual Result<std::unique_ptr<Aggregator>> NewAggregator() const = 0;
+
+ protected:
+  BoundAggregate(Expression::Operation op, std::shared_ptr<BoundTerm> term)
+      : Aggregate<BoundTerm>(op, std::move(term)) {}
+};
+
+/// \brief Base class for COUNT aggregates.
+class ICEBERG_EXPORT CountAggregate : public BoundAggregate {
+ public:
+  Kind kind() const override { return Kind::kCount; }
+
+ protected:
+  CountAggregate(Expression::Operation op, std::shared_ptr<BoundTerm> term)
+      : BoundAggregate(op, std::move(term)) {}
+};
+
+/// \brief COUNT(term) aggregate.
+class ICEBERG_EXPORT CountNonNullAggregate : public CountAggregate {
+ public:
+  static Result<std::shared_ptr<CountNonNullAggregate>> Make(
+      std::shared_ptr<BoundTerm> term);
+
+  std::string ToString() const override;
+  Result<Literal> Evaluate(const StructLike& data) const override;
+  Result<std::unique_ptr<Aggregator>> NewAggregator() const override;
+
+ private:
+  explicit CountNonNullAggregate(std::shared_ptr<BoundTerm> term);
+};
+
+/// \brief COUNT_NULL(term) aggregate.
+class ICEBERG_EXPORT CountNullAggregate : public CountAggregate {
+ public:
+  static Result<std::shared_ptr<CountNullAggregate>> Make(
+      std::shared_ptr<BoundTerm> term);
+
+  std::string ToString() const override;
+  Result<Literal> Evaluate(const StructLike& data) const override;
+  Result<std::unique_ptr<Aggregator>> NewAggregator() const override;
+
+ private:
+  explicit CountNullAggregate(std::shared_ptr<BoundTerm> term);
+};
+
+/// \brief COUNT(*) aggregate.
+class ICEBERG_EXPORT CountStarAggregate : public CountAggregate {
+ public:
+  static Result<std::shared_ptr<CountStarAggregate>> Make();
+
+  std::string ToString() const override;
+  Result<Literal> Evaluate(const StructLike& data) const override;
+  Result<std::unique_ptr<Aggregator>> NewAggregator() const override;
+
+ private:
+  CountStarAggregate();
+};
+
+/// \brief Bound MAX/MIN aggregate.
+class ICEBERG_EXPORT ValueAggregate : public BoundAggregate {

Review Comment:
   > Why not adding subclasses for MIN and MAX for ValueAggregate and ValueAggregator?
   
   That makes sense. I’ll split the current `ValueAggregate` & `ValueAggregator` into separate `Min` & `Max` subclasses so that each operation has its own concrete type and we don’t need the `Kind` enum to distinguish them. This will also better match the Java implementation.



-- 
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: issues-unsubscribe@iceberg.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org


Re: [PR] feat: add aggregate expressions and evaluator [iceberg-cpp]

Posted by "SuKi2cn (via GitHub)" <gi...@apache.org>.
SuKi2cn commented on code in PR #335:
URL: https://github.com/apache/iceberg-cpp/pull/335#discussion_r2564214209


##########
src/iceberg/test/aggregate_test.cc:
##########
@@ -0,0 +1,160 @@
+/*
+ * 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/aggregate.h"
+
+#include <gtest/gtest.h>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/expression/expressions.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/schema.h"
+#include "iceberg/test/matchers.h"
+#include "iceberg/type.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {

Review Comment:
   > These test cases look insufficient to me. I think we need to covert at least the following:
   > 
   > * Verify unbound aggregates of different operations are created successfully and invalid inputs are rejected.
   > * Verify binding unbound to bound aggregates work as expected for different operations.
   > * Verify ToString and other member functions work.
   > * Verify bound aggregates evaluation work as expected.
   > * Verify different evaluators work fine for all operations and single/multiple aggregate inputs.
   
   Thank you for your review. I agree the current test coverage is not sufficient.
   I will expand the test cases to cover the scenarios you listed, including creation and validation of unbound aggregates, binding behavior across different operations, ToString and other member functions, as well as evaluation correctness for both single and multiple aggregates.



-- 
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: issues-unsubscribe@iceberg.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org


Re: [PR] feat: add aggregate expressions and evaluator [iceberg-cpp]

Posted by "wgtmac (via GitHub)" <gi...@apache.org>.
wgtmac commented on code in PR #335:
URL: https://github.com/apache/iceberg-cpp/pull/335#discussion_r2559189487


##########
src/iceberg/expression/aggregate.h:
##########
@@ -0,0 +1,230 @@
+/*
+ * 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/aggregate.h
+/// Aggregate expression definitions.
+
+#include <memory>
+#include <optional>
+#include <string>
+#include <vector>
+
+#include "iceberg/expression/expression.h"
+#include "iceberg/expression/term.h"
+#include "iceberg/result.h"
+#include "iceberg/type_fwd.h"
+
+namespace iceberg {
+
+/// \brief Base aggregate holding an operation and a term.
+template <TermType T>
+class ICEBERG_EXPORT Aggregate : public virtual Expression {
+ public:
+  ~Aggregate() override = default;
+
+  Expression::Operation op() const override { return operation_; }
+
+  const std::shared_ptr<T>& term() const { return term_; }
+
+ protected:
+  Aggregate(Expression::Operation op, std::shared_ptr<T> term)
+      : operation_(op), term_(std::move(term)) {}
+
+  Expression::Operation operation_;
+  std::shared_ptr<T> term_;
+};
+
+/// \brief Base class for unbound aggregates.
+class ICEBERG_EXPORT UnboundAggregate : public virtual Expression,
+                                        public Unbound<Expression> {
+ public:
+  ~UnboundAggregate() override = default;
+
+  bool is_unbound_aggregate() const override { return true; }
+};
+
+/// \brief Template for unbound aggregates that carry a term and operation.
+template <typename B>
+class ICEBERG_EXPORT UnboundAggregateImpl : public UnboundAggregate,
+                                            public Aggregate<UnboundTerm<B>> {
+  using BASE = Aggregate<UnboundTerm<B>>;
+
+ public:
+  static Result<std::shared_ptr<UnboundAggregateImpl<B>>> Make(
+      Expression::Operation op, std::shared_ptr<UnboundTerm<B>> term);
+
+  std::shared_ptr<NamedReference> reference() override {
+    return BASE::term() ? BASE::term()->reference() : nullptr;
+  }
+
+  Result<std::shared_ptr<Expression>> Bind(const Schema& schema,
+                                           bool case_sensitive) const override;
+
+  std::string ToString() const override;
+
+ private:
+  static constexpr bool IsSupportedOp(Expression::Operation op) {
+    return op == Expression::Operation::kCount ||
+           op == Expression::Operation::kCountNull ||
+           op == Expression::Operation::kCountStar || op == Expression::Operation::kMax ||
+           op == Expression::Operation::kMin;
+  }
+
+  UnboundAggregateImpl(Expression::Operation op, std::shared_ptr<UnboundTerm<B>> term)
+      : BASE(op, std::move(term)) {
+    ICEBERG_DCHECK(IsSupportedOp(op), "Unexpected aggregate operation");
+    ICEBERG_DCHECK(op == Expression::Operation::kCountStar || BASE::term() != nullptr,
+                   "Aggregate term cannot be null unless COUNT(*)");
+  }
+};
+
+/// \brief Base class for bound aggregates.
+class ICEBERG_EXPORT BoundAggregate : public Aggregate<BoundTerm>, public Bound {
+ public:
+  using Aggregate<BoundTerm>::op;
+  using Aggregate<BoundTerm>::term;
+
+  class ICEBERG_EXPORT Aggregator {
+   public:
+    virtual ~Aggregator() = default;
+
+    virtual Status Update(const StructLike& row) = 0;
+    virtual Status Update(const DataFile& file) {
+      return NotSupported("Aggregating DataFile not supported");
+    }
+    virtual Result<Literal> ResultLiteral() const = 0;
+  };
+
+  std::shared_ptr<BoundReference> reference() override {
+    ICEBERG_DCHECK(term_ != nullptr || op() == Expression::Operation::kCountStar,
+                   "Bound aggregate term should not be null unless COUNT(*)");
+    return term_ ? term_->reference() : nullptr;
+  }
+
+  Result<Literal> Evaluate(const StructLike& data) const override = 0;
+
+  bool is_bound_aggregate() const override { return true; }
+
+  enum class Kind : int8_t {
+    // Count aggregates (COUNT, COUNT_STAR, COUNT_NULL)
+    kCount = 0,
+    // Value aggregates (MIN, MAX)
+    kValue,
+  };
+
+  virtual Kind kind() const = 0;
+  virtual Result<std::unique_ptr<Aggregator>> NewAggregator() const = 0;
+
+ protected:
+  BoundAggregate(Expression::Operation op, std::shared_ptr<BoundTerm> term)
+      : Aggregate<BoundTerm>(op, std::move(term)) {}
+};
+
+/// \brief Base class for COUNT aggregates.
+class ICEBERG_EXPORT CountAggregate : public BoundAggregate {
+ public:
+  Kind kind() const override { return Kind::kCount; }
+
+ protected:
+  CountAggregate(Expression::Operation op, std::shared_ptr<BoundTerm> term)
+      : BoundAggregate(op, std::move(term)) {}
+};
+
+/// \brief COUNT(term) aggregate.
+class ICEBERG_EXPORT CountNonNullAggregate : public CountAggregate {
+ public:
+  static Result<std::shared_ptr<CountNonNullAggregate>> Make(
+      std::shared_ptr<BoundTerm> term);
+
+  std::string ToString() const override;
+  Result<Literal> Evaluate(const StructLike& data) const override;
+  Result<std::unique_ptr<Aggregator>> NewAggregator() const override;
+
+ private:
+  explicit CountNonNullAggregate(std::shared_ptr<BoundTerm> term);
+};
+
+/// \brief COUNT_NULL(term) aggregate.
+class ICEBERG_EXPORT CountNullAggregate : public CountAggregate {
+ public:
+  static Result<std::shared_ptr<CountNullAggregate>> Make(
+      std::shared_ptr<BoundTerm> term);
+
+  std::string ToString() const override;
+  Result<Literal> Evaluate(const StructLike& data) const override;
+  Result<std::unique_ptr<Aggregator>> NewAggregator() const override;
+
+ private:
+  explicit CountNullAggregate(std::shared_ptr<BoundTerm> term);
+};
+
+/// \brief COUNT(*) aggregate.
+class ICEBERG_EXPORT CountStarAggregate : public CountAggregate {
+ public:
+  static Result<std::shared_ptr<CountStarAggregate>> Make();
+
+  std::string ToString() const override;
+  Result<Literal> Evaluate(const StructLike& data) const override;
+  Result<std::unique_ptr<Aggregator>> NewAggregator() const override;
+
+ private:
+  CountStarAggregate();
+};
+
+/// \brief Bound MAX/MIN aggregate.
+class ICEBERG_EXPORT ValueAggregate : public BoundAggregate {
+ public:
+  ValueAggregate(Expression::Operation op, std::shared_ptr<BoundTerm> term);
+
+  Kind kind() const override { return Kind::kValue; }
+
+  std::string ToString() const override;
+  Result<Literal> Evaluate(const StructLike& data) const override;
+  Result<std::unique_ptr<Aggregator>> NewAggregator() const override;
+};
+
+/// \brief Evaluates bound aggregates over StructLike rows.
+class ICEBERG_EXPORT AggregateEvaluator {
+ public:
+  virtual ~AggregateEvaluator() = default;
+
+  /// \brief Create an evaluator for a single bound aggregate.
+  /// \param aggregate The bound aggregate to evaluate across rows.
+  static Result<std::unique_ptr<AggregateEvaluator>> Make(
+      std::shared_ptr<BoundAggregate> aggregate);
+
+  /// \brief Create an evaluator for multiple bound aggregates.
+  /// \param aggregates Aggregates to evaluate in one pass; order is preserved in
+  /// Results().
+  static Result<std::unique_ptr<AggregateEvaluator>> MakeList(
+      std::vector<std::shared_ptr<BoundAggregate>> aggregates);
+
+  /// \brief Update aggregates with a row.
+  virtual Status Update(const StructLike& row) = 0;
+
+  /// \brief Final aggregated value.
+  virtual Result<std::vector<Literal>> Results() const = 0;

Review Comment:
   Should we return `Result<std::span<const Literal>>`?



##########
src/iceberg/expression/aggregate.cc:
##########
@@ -0,0 +1,398 @@
+/*
+ * 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/aggregate.h"
+
+#include <format>
+#include <optional>
+#include <vector>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/type.h"
+#include "iceberg/util/checked_cast.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+Result<std::shared_ptr<PrimitiveType>> GetPrimitiveType(const BoundTerm& term) {
+  if (!term.type()->is_primitive()) {
+    return InvalidExpression("Aggregate requires primitive type, got {}",
+                             term.type()->ToString());
+  }
+  return internal::checked_pointer_cast<PrimitiveType>(term.type());
+}
+
+class CountNonNullAggregator : public BoundAggregate::Aggregator {
+ public:
+  explicit CountNonNullAggregator(std::shared_ptr<BoundTerm> term)
+      : term_(std::move(term)) {}
+
+  Status Update(const StructLike& row) override {
+    ICEBERG_ASSIGN_OR_RAISE(auto literal, term_->Evaluate(row));
+    if (!literal.IsNull()) {
+      ++count_;
+    }
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override { return Literal::Long(count_); }
+
+ private:
+  std::shared_ptr<BoundTerm> term_;
+  int64_t count_ = 0;
+};
+
+class CountNullAggregator : public BoundAggregate::Aggregator {
+ public:
+  explicit CountNullAggregator(std::shared_ptr<BoundTerm> term)
+      : term_(std::move(term)) {}
+
+  Status Update(const StructLike& row) override {
+    ICEBERG_ASSIGN_OR_RAISE(auto literal, term_->Evaluate(row));
+    if (literal.IsNull()) {
+      ++count_;
+    }
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override { return Literal::Long(count_); }
+
+ private:
+  std::shared_ptr<BoundTerm> term_;
+  int64_t count_ = 0;
+};
+
+class CountStarAggregator : public BoundAggregate::Aggregator {
+ public:
+  Status Update(const StructLike& /*row*/) override {
+    ++count_;
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override { return Literal::Long(count_); }
+
+ private:
+  int64_t count_ = 0;
+};
+
+class ValueAggregatorImpl : public BoundAggregate::Aggregator {
+ public:
+  ValueAggregatorImpl(bool is_max, std::shared_ptr<BoundTerm> term)
+      : is_max_(is_max), term_(std::move(term)) {}
+
+  Status Update(const StructLike& row) override {
+    ICEBERG_ASSIGN_OR_RAISE(auto val_literal, term_->Evaluate(row));
+    if (val_literal.IsNull()) {
+      return {};
+    }
+    if (!current_) {
+      current_ = std::move(val_literal);
+      return {};
+    }
+
+    auto ordering = val_literal <=> *current_;
+    if (ordering == std::partial_ordering::unordered) {
+      return InvalidExpression("Cannot compare literals of type {}",
+                               val_literal.type()->ToString());
+    }
+
+    if (is_max_) {
+      if (ordering == std::partial_ordering::greater) {
+        current_ = std::move(val_literal);
+      }
+    } else {
+      if (ordering == std::partial_ordering::less) {
+        current_ = std::move(val_literal);
+      }
+    }
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override {
+    if (current_) {
+      return *current_;
+    }
+    ICEBERG_ASSIGN_OR_RAISE(auto type, GetPrimitiveType(*term_));
+    return Literal::Null(type);
+  }
+
+ private:
+  bool is_max_;
+  std::shared_ptr<BoundTerm> term_;
+  std::optional<Literal> current_;
+};
+
+}  // namespace
+
+// -------------------- Bound aggregates --------------------
+
+CountNonNullAggregate::CountNonNullAggregate(std::shared_ptr<BoundTerm> term)
+    : CountAggregate(Expression::Operation::kCount, std::move(term)) {}
+
+Result<std::shared_ptr<CountNonNullAggregate>> CountNonNullAggregate::Make(
+    std::shared_ptr<BoundTerm> term) {
+  if (!term) {
+    return InvalidExpression("Bound count aggregate requires non-null term");
+  }
+  return std::shared_ptr<CountNonNullAggregate>(
+      new CountNonNullAggregate(std::move(term)));
+}
+
+std::string CountNonNullAggregate::ToString() const {
+  ICEBERG_DCHECK(term() != nullptr, "Bound count aggregate should have term");
+  return std::format("count({})", term()->reference()->name());
+}
+
+Result<Literal> CountNonNullAggregate::Evaluate(const StructLike& data) const {
+  ICEBERG_ASSIGN_OR_RAISE(auto literal, term()->Evaluate(data));
+  return Literal::Long(literal.IsNull() ? 0 : 1);
+}
+
+Result<std::unique_ptr<BoundAggregate::Aggregator>> CountNonNullAggregate::NewAggregator()
+    const {
+  return std::unique_ptr<BoundAggregate::Aggregator>(new CountNonNullAggregator(term()));
+}
+
+CountNullAggregate::CountNullAggregate(std::shared_ptr<BoundTerm> term)
+    : CountAggregate(Expression::Operation::kCountNull, std::move(term)) {}
+
+Result<std::shared_ptr<CountNullAggregate>> CountNullAggregate::Make(
+    std::shared_ptr<BoundTerm> term) {
+  if (!term) {
+    return InvalidExpression("Bound count aggregate requires non-null term");
+  }
+  return std::shared_ptr<CountNullAggregate>(new CountNullAggregate(std::move(term)));
+}
+
+std::string CountNullAggregate::ToString() const {
+  ICEBERG_DCHECK(term() != nullptr, "Bound count aggregate should have term");
+  return std::format("count_null({})", term()->reference()->name());
+}
+
+Result<Literal> CountNullAggregate::Evaluate(const StructLike& data) const {
+  ICEBERG_ASSIGN_OR_RAISE(auto literal, term()->Evaluate(data));
+  return Literal::Long(literal.IsNull() ? 1 : 0);
+}
+
+Result<std::unique_ptr<BoundAggregate::Aggregator>> CountNullAggregate::NewAggregator()
+    const {
+  return std::unique_ptr<BoundAggregate::Aggregator>(new CountNullAggregator(term()));
+}
+
+CountStarAggregate::CountStarAggregate()
+    : CountAggregate(Expression::Operation::kCountStar, nullptr) {}
+
+Result<std::shared_ptr<CountStarAggregate>> CountStarAggregate::Make() {
+  return std::shared_ptr<CountStarAggregate>(new CountStarAggregate());
+}
+
+std::string CountStarAggregate::ToString() const { return "count(*)"; }
+
+Result<Literal> CountStarAggregate::Evaluate(const StructLike& data) const {
+  return Literal::Long(1);
+}
+
+Result<std::unique_ptr<BoundAggregate::Aggregator>> CountStarAggregate::NewAggregator()
+    const {
+  return std::unique_ptr<BoundAggregate::Aggregator>(new CountStarAggregator());
+}
+
+ValueAggregate::ValueAggregate(Expression::Operation op, std::shared_ptr<BoundTerm> term)
+    : BoundAggregate(op, std::move(term)) {}
+
+std::string ValueAggregate::ToString() const {
+  ICEBERG_DCHECK(term() != nullptr, "Bound value aggregate should have term");
+  auto prefix = op() == Expression::Operation::kMax ? "max" : "min";
+  return std::format("{}({})", prefix, term()->reference()->name());
+}
+
+Result<Literal> ValueAggregate::Evaluate(const StructLike& data) const {
+  ICEBERG_ASSIGN_OR_RAISE(auto literal, term()->Evaluate(data));
+  return literal;
+}
+
+Result<std::unique_ptr<BoundAggregate::Aggregator>> ValueAggregate::NewAggregator()
+    const {
+  bool is_max = op() == Expression::Operation::kMax;
+  return std::unique_ptr<BoundAggregate::Aggregator>(
+      new ValueAggregatorImpl(is_max, term()));
+}
+
+// -------------------- Unbound binding --------------------
+
+template <typename B>
+Result<std::shared_ptr<Expression>> UnboundAggregateImpl<B>::Bind(
+    const Schema& schema, bool case_sensitive) const {
+  ICEBERG_DCHECK(UnboundAggregateImpl<B>::IsSupportedOp(this->op()),
+                 "Unexpected aggregate operation");
+
+  std::shared_ptr<B> bound_term;
+  if (this->term()) {
+    ICEBERG_ASSIGN_OR_THROW(bound_term, this->term()->Bind(schema, case_sensitive));
+  }
+
+  switch (this->op()) {
+    case Expression::Operation::kCountStar: {
+      ICEBERG_ASSIGN_OR_THROW(auto aggregate, CountStarAggregate::Make());
+      return aggregate;
+    }
+    case Expression::Operation::kCount: {
+      if (!bound_term) {
+        return InvalidExpression("Aggregate requires a term");
+      }
+      ICEBERG_ASSIGN_OR_THROW(auto aggregate,
+                              CountNonNullAggregate::Make(std::move(bound_term)));
+      return aggregate;
+    }
+    case Expression::Operation::kCountNull: {
+      if (!bound_term) {
+        return InvalidExpression("Aggregate requires a term");
+      }
+      ICEBERG_ASSIGN_OR_THROW(auto aggregate,
+                              CountNullAggregate::Make(std::move(bound_term)));
+      return aggregate;
+    }
+    case Expression::Operation::kMax:
+    case Expression::Operation::kMin: {
+      if (!bound_term) {
+        return InvalidExpression("Aggregate requires a term");
+      }
+      auto aggregate =
+          std::make_shared<ValueAggregate>(this->op(), std::move(bound_term));
+      return aggregate;
+    }
+    default:
+      return NotSupported("Unsupported aggregate operation");
+  }
+}
+
+template <typename B>
+Result<std::shared_ptr<UnboundAggregateImpl<B>>> UnboundAggregateImpl<B>::Make(
+    Expression::Operation op, std::shared_ptr<UnboundTerm<B>> term) {
+  if (!IsSupportedOp(op)) {
+    return NotSupported("Unsupported aggregate operation: {}", ::iceberg::ToString(op));
+  }
+  if (op != Expression::Operation::kCountStar && !term) {
+    return InvalidExpression("Aggregate term cannot be null unless COUNT(*)");
+  }
+
+  return std::shared_ptr<UnboundAggregateImpl<B>>(
+      new UnboundAggregateImpl<B>(op, std::move(term)));
+}
+
+template <typename B>
+std::string UnboundAggregateImpl<B>::ToString() const {

Review Comment:
   Can we move this to `Aggregate::ToString()` so all other overrides can be removed.



##########
src/iceberg/expression/aggregate.cc:
##########
@@ -0,0 +1,398 @@
+/*
+ * 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/aggregate.h"
+
+#include <format>
+#include <optional>
+#include <vector>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/type.h"
+#include "iceberg/util/checked_cast.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+Result<std::shared_ptr<PrimitiveType>> GetPrimitiveType(const BoundTerm& term) {
+  if (!term.type()->is_primitive()) {
+    return InvalidExpression("Aggregate requires primitive type, got {}",
+                             term.type()->ToString());
+  }
+  return internal::checked_pointer_cast<PrimitiveType>(term.type());
+}
+
+class CountNonNullAggregator : public BoundAggregate::Aggregator {
+ public:
+  explicit CountNonNullAggregator(std::shared_ptr<BoundTerm> term)
+      : term_(std::move(term)) {}
+
+  Status Update(const StructLike& row) override {
+    ICEBERG_ASSIGN_OR_RAISE(auto literal, term_->Evaluate(row));
+    if (!literal.IsNull()) {
+      ++count_;
+    }
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override { return Literal::Long(count_); }
+
+ private:
+  std::shared_ptr<BoundTerm> term_;
+  int64_t count_ = 0;
+};
+
+class CountNullAggregator : public BoundAggregate::Aggregator {
+ public:
+  explicit CountNullAggregator(std::shared_ptr<BoundTerm> term)
+      : term_(std::move(term)) {}
+
+  Status Update(const StructLike& row) override {
+    ICEBERG_ASSIGN_OR_RAISE(auto literal, term_->Evaluate(row));
+    if (literal.IsNull()) {
+      ++count_;
+    }
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override { return Literal::Long(count_); }
+
+ private:
+  std::shared_ptr<BoundTerm> term_;
+  int64_t count_ = 0;
+};
+
+class CountStarAggregator : public BoundAggregate::Aggregator {
+ public:
+  Status Update(const StructLike& /*row*/) override {
+    ++count_;
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override { return Literal::Long(count_); }
+
+ private:
+  int64_t count_ = 0;
+};
+
+class ValueAggregatorImpl : public BoundAggregate::Aggregator {
+ public:
+  ValueAggregatorImpl(bool is_max, std::shared_ptr<BoundTerm> term)
+      : is_max_(is_max), term_(std::move(term)) {}
+
+  Status Update(const StructLike& row) override {
+    ICEBERG_ASSIGN_OR_RAISE(auto val_literal, term_->Evaluate(row));
+    if (val_literal.IsNull()) {
+      return {};
+    }
+    if (!current_) {
+      current_ = std::move(val_literal);
+      return {};
+    }
+
+    auto ordering = val_literal <=> *current_;
+    if (ordering == std::partial_ordering::unordered) {
+      return InvalidExpression("Cannot compare literals of type {}",
+                               val_literal.type()->ToString());
+    }
+
+    if (is_max_) {
+      if (ordering == std::partial_ordering::greater) {
+        current_ = std::move(val_literal);
+      }
+    } else {
+      if (ordering == std::partial_ordering::less) {
+        current_ = std::move(val_literal);
+      }
+    }
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override {
+    if (current_) {
+      return *current_;
+    }
+    ICEBERG_ASSIGN_OR_RAISE(auto type, GetPrimitiveType(*term_));
+    return Literal::Null(type);
+  }
+
+ private:
+  bool is_max_;
+  std::shared_ptr<BoundTerm> term_;
+  std::optional<Literal> current_;
+};
+
+}  // namespace
+
+// -------------------- Bound aggregates --------------------
+
+CountNonNullAggregate::CountNonNullAggregate(std::shared_ptr<BoundTerm> term)
+    : CountAggregate(Expression::Operation::kCount, std::move(term)) {}
+
+Result<std::shared_ptr<CountNonNullAggregate>> CountNonNullAggregate::Make(
+    std::shared_ptr<BoundTerm> term) {
+  if (!term) {
+    return InvalidExpression("Bound count aggregate requires non-null term");
+  }
+  return std::shared_ptr<CountNonNullAggregate>(
+      new CountNonNullAggregate(std::move(term)));
+}
+
+std::string CountNonNullAggregate::ToString() const {
+  ICEBERG_DCHECK(term() != nullptr, "Bound count aggregate should have term");
+  return std::format("count({})", term()->reference()->name());
+}
+
+Result<Literal> CountNonNullAggregate::Evaluate(const StructLike& data) const {
+  ICEBERG_ASSIGN_OR_RAISE(auto literal, term()->Evaluate(data));
+  return Literal::Long(literal.IsNull() ? 0 : 1);
+}
+
+Result<std::unique_ptr<BoundAggregate::Aggregator>> CountNonNullAggregate::NewAggregator()
+    const {
+  return std::unique_ptr<BoundAggregate::Aggregator>(new CountNonNullAggregator(term()));
+}
+
+CountNullAggregate::CountNullAggregate(std::shared_ptr<BoundTerm> term)
+    : CountAggregate(Expression::Operation::kCountNull, std::move(term)) {}
+
+Result<std::shared_ptr<CountNullAggregate>> CountNullAggregate::Make(
+    std::shared_ptr<BoundTerm> term) {
+  if (!term) {
+    return InvalidExpression("Bound count aggregate requires non-null term");
+  }
+  return std::shared_ptr<CountNullAggregate>(new CountNullAggregate(std::move(term)));
+}
+
+std::string CountNullAggregate::ToString() const {
+  ICEBERG_DCHECK(term() != nullptr, "Bound count aggregate should have term");
+  return std::format("count_null({})", term()->reference()->name());
+}
+
+Result<Literal> CountNullAggregate::Evaluate(const StructLike& data) const {
+  ICEBERG_ASSIGN_OR_RAISE(auto literal, term()->Evaluate(data));
+  return Literal::Long(literal.IsNull() ? 1 : 0);
+}
+
+Result<std::unique_ptr<BoundAggregate::Aggregator>> CountNullAggregate::NewAggregator()
+    const {
+  return std::unique_ptr<BoundAggregate::Aggregator>(new CountNullAggregator(term()));
+}
+
+CountStarAggregate::CountStarAggregate()
+    : CountAggregate(Expression::Operation::kCountStar, nullptr) {}
+
+Result<std::shared_ptr<CountStarAggregate>> CountStarAggregate::Make() {
+  return std::shared_ptr<CountStarAggregate>(new CountStarAggregate());
+}
+
+std::string CountStarAggregate::ToString() const { return "count(*)"; }
+
+Result<Literal> CountStarAggregate::Evaluate(const StructLike& data) const {
+  return Literal::Long(1);
+}
+
+Result<std::unique_ptr<BoundAggregate::Aggregator>> CountStarAggregate::NewAggregator()
+    const {
+  return std::unique_ptr<BoundAggregate::Aggregator>(new CountStarAggregator());
+}
+
+ValueAggregate::ValueAggregate(Expression::Operation op, std::shared_ptr<BoundTerm> term)
+    : BoundAggregate(op, std::move(term)) {}
+
+std::string ValueAggregate::ToString() const {
+  ICEBERG_DCHECK(term() != nullptr, "Bound value aggregate should have term");
+  auto prefix = op() == Expression::Operation::kMax ? "max" : "min";
+  return std::format("{}({})", prefix, term()->reference()->name());
+}
+
+Result<Literal> ValueAggregate::Evaluate(const StructLike& data) const {
+  ICEBERG_ASSIGN_OR_RAISE(auto literal, term()->Evaluate(data));
+  return literal;
+}
+
+Result<std::unique_ptr<BoundAggregate::Aggregator>> ValueAggregate::NewAggregator()
+    const {
+  bool is_max = op() == Expression::Operation::kMax;
+  return std::unique_ptr<BoundAggregate::Aggregator>(
+      new ValueAggregatorImpl(is_max, term()));
+}
+
+// -------------------- Unbound binding --------------------
+
+template <typename B>
+Result<std::shared_ptr<Expression>> UnboundAggregateImpl<B>::Bind(
+    const Schema& schema, bool case_sensitive) const {
+  ICEBERG_DCHECK(UnboundAggregateImpl<B>::IsSupportedOp(this->op()),
+                 "Unexpected aggregate operation");
+
+  std::shared_ptr<B> bound_term;
+  if (this->term()) {
+    ICEBERG_ASSIGN_OR_THROW(bound_term, this->term()->Bind(schema, case_sensitive));
+  }
+
+  switch (this->op()) {
+    case Expression::Operation::kCountStar: {
+      ICEBERG_ASSIGN_OR_THROW(auto aggregate, CountStarAggregate::Make());
+      return aggregate;
+    }

Review Comment:
   ```suggestion
       case Expression::Operation::kCountStar:
         return CountStarAggregate::Make();
   ```
   
   Same for below.



##########
src/iceberg/expression/aggregate.cc:
##########
@@ -0,0 +1,398 @@
+/*
+ * 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/aggregate.h"
+
+#include <format>
+#include <optional>
+#include <vector>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/type.h"
+#include "iceberg/util/checked_cast.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+Result<std::shared_ptr<PrimitiveType>> GetPrimitiveType(const BoundTerm& term) {
+  if (!term.type()->is_primitive()) {
+    return InvalidExpression("Aggregate requires primitive type, got {}",
+                             term.type()->ToString());
+  }
+  return internal::checked_pointer_cast<PrimitiveType>(term.type());
+}
+
+class CountNonNullAggregator : public BoundAggregate::Aggregator {
+ public:
+  explicit CountNonNullAggregator(std::shared_ptr<BoundTerm> term)
+      : term_(std::move(term)) {}
+
+  Status Update(const StructLike& row) override {
+    ICEBERG_ASSIGN_OR_RAISE(auto literal, term_->Evaluate(row));
+    if (!literal.IsNull()) {
+      ++count_;
+    }
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override { return Literal::Long(count_); }
+
+ private:
+  std::shared_ptr<BoundTerm> term_;
+  int64_t count_ = 0;
+};
+
+class CountNullAggregator : public BoundAggregate::Aggregator {
+ public:
+  explicit CountNullAggregator(std::shared_ptr<BoundTerm> term)
+      : term_(std::move(term)) {}
+
+  Status Update(const StructLike& row) override {
+    ICEBERG_ASSIGN_OR_RAISE(auto literal, term_->Evaluate(row));
+    if (literal.IsNull()) {
+      ++count_;
+    }
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override { return Literal::Long(count_); }
+
+ private:
+  std::shared_ptr<BoundTerm> term_;
+  int64_t count_ = 0;
+};
+
+class CountStarAggregator : public BoundAggregate::Aggregator {
+ public:
+  Status Update(const StructLike& /*row*/) override {
+    ++count_;
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override { return Literal::Long(count_); }
+
+ private:
+  int64_t count_ = 0;
+};
+
+class ValueAggregatorImpl : public BoundAggregate::Aggregator {
+ public:
+  ValueAggregatorImpl(bool is_max, std::shared_ptr<BoundTerm> term)
+      : is_max_(is_max), term_(std::move(term)) {}
+
+  Status Update(const StructLike& row) override {
+    ICEBERG_ASSIGN_OR_RAISE(auto val_literal, term_->Evaluate(row));
+    if (val_literal.IsNull()) {
+      return {};
+    }
+    if (!current_) {
+      current_ = std::move(val_literal);
+      return {};
+    }
+
+    auto ordering = val_literal <=> *current_;
+    if (ordering == std::partial_ordering::unordered) {
+      return InvalidExpression("Cannot compare literals of type {}",
+                               val_literal.type()->ToString());
+    }
+
+    if (is_max_) {
+      if (ordering == std::partial_ordering::greater) {
+        current_ = std::move(val_literal);
+      }
+    } else {
+      if (ordering == std::partial_ordering::less) {
+        current_ = std::move(val_literal);
+      }
+    }
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override {
+    if (current_) {
+      return *current_;
+    }
+    ICEBERG_ASSIGN_OR_RAISE(auto type, GetPrimitiveType(*term_));
+    return Literal::Null(type);
+  }
+
+ private:
+  bool is_max_;
+  std::shared_ptr<BoundTerm> term_;
+  std::optional<Literal> current_;
+};
+
+}  // namespace
+
+// -------------------- Bound aggregates --------------------
+
+CountNonNullAggregate::CountNonNullAggregate(std::shared_ptr<BoundTerm> term)
+    : CountAggregate(Expression::Operation::kCount, std::move(term)) {}
+
+Result<std::shared_ptr<CountNonNullAggregate>> CountNonNullAggregate::Make(
+    std::shared_ptr<BoundTerm> term) {
+  if (!term) {
+    return InvalidExpression("Bound count aggregate requires non-null term");
+  }
+  return std::shared_ptr<CountNonNullAggregate>(
+      new CountNonNullAggregate(std::move(term)));
+}
+
+std::string CountNonNullAggregate::ToString() const {
+  ICEBERG_DCHECK(term() != nullptr, "Bound count aggregate should have term");
+  return std::format("count({})", term()->reference()->name());
+}
+
+Result<Literal> CountNonNullAggregate::Evaluate(const StructLike& data) const {
+  ICEBERG_ASSIGN_OR_RAISE(auto literal, term()->Evaluate(data));
+  return Literal::Long(literal.IsNull() ? 0 : 1);
+}
+
+Result<std::unique_ptr<BoundAggregate::Aggregator>> CountNonNullAggregate::NewAggregator()
+    const {
+  return std::unique_ptr<BoundAggregate::Aggregator>(new CountNonNullAggregator(term()));
+}
+
+CountNullAggregate::CountNullAggregate(std::shared_ptr<BoundTerm> term)
+    : CountAggregate(Expression::Operation::kCountNull, std::move(term)) {}
+
+Result<std::shared_ptr<CountNullAggregate>> CountNullAggregate::Make(
+    std::shared_ptr<BoundTerm> term) {
+  if (!term) {
+    return InvalidExpression("Bound count aggregate requires non-null term");
+  }
+  return std::shared_ptr<CountNullAggregate>(new CountNullAggregate(std::move(term)));
+}
+
+std::string CountNullAggregate::ToString() const {
+  ICEBERG_DCHECK(term() != nullptr, "Bound count aggregate should have term");
+  return std::format("count_null({})", term()->reference()->name());
+}
+
+Result<Literal> CountNullAggregate::Evaluate(const StructLike& data) const {
+  ICEBERG_ASSIGN_OR_RAISE(auto literal, term()->Evaluate(data));
+  return Literal::Long(literal.IsNull() ? 1 : 0);
+}
+
+Result<std::unique_ptr<BoundAggregate::Aggregator>> CountNullAggregate::NewAggregator()
+    const {
+  return std::unique_ptr<BoundAggregate::Aggregator>(new CountNullAggregator(term()));
+}
+
+CountStarAggregate::CountStarAggregate()
+    : CountAggregate(Expression::Operation::kCountStar, nullptr) {}
+
+Result<std::shared_ptr<CountStarAggregate>> CountStarAggregate::Make() {
+  return std::shared_ptr<CountStarAggregate>(new CountStarAggregate());
+}
+
+std::string CountStarAggregate::ToString() const { return "count(*)"; }
+
+Result<Literal> CountStarAggregate::Evaluate(const StructLike& data) const {
+  return Literal::Long(1);
+}
+
+Result<std::unique_ptr<BoundAggregate::Aggregator>> CountStarAggregate::NewAggregator()
+    const {
+  return std::unique_ptr<BoundAggregate::Aggregator>(new CountStarAggregator());
+}
+
+ValueAggregate::ValueAggregate(Expression::Operation op, std::shared_ptr<BoundTerm> term)
+    : BoundAggregate(op, std::move(term)) {}
+
+std::string ValueAggregate::ToString() const {
+  ICEBERG_DCHECK(term() != nullptr, "Bound value aggregate should have term");
+  auto prefix = op() == Expression::Operation::kMax ? "max" : "min";
+  return std::format("{}({})", prefix, term()->reference()->name());
+}
+
+Result<Literal> ValueAggregate::Evaluate(const StructLike& data) const {
+  ICEBERG_ASSIGN_OR_RAISE(auto literal, term()->Evaluate(data));
+  return literal;
+}
+
+Result<std::unique_ptr<BoundAggregate::Aggregator>> ValueAggregate::NewAggregator()
+    const {
+  bool is_max = op() == Expression::Operation::kMax;
+  return std::unique_ptr<BoundAggregate::Aggregator>(
+      new ValueAggregatorImpl(is_max, term()));
+}
+
+// -------------------- Unbound binding --------------------
+
+template <typename B>
+Result<std::shared_ptr<Expression>> UnboundAggregateImpl<B>::Bind(
+    const Schema& schema, bool case_sensitive) const {
+  ICEBERG_DCHECK(UnboundAggregateImpl<B>::IsSupportedOp(this->op()),
+                 "Unexpected aggregate operation");
+
+  std::shared_ptr<B> bound_term;
+  if (this->term()) {
+    ICEBERG_ASSIGN_OR_THROW(bound_term, this->term()->Bind(schema, case_sensitive));

Review Comment:
   We cannot throw in this function. Please use `ICEBERG_ASSIGN_OR_RAISE` for this. Same for below.



##########
src/iceberg/expression/aggregate.cc:
##########
@@ -0,0 +1,398 @@
+/*
+ * 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/aggregate.h"
+
+#include <format>
+#include <optional>
+#include <vector>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/type.h"
+#include "iceberg/util/checked_cast.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+Result<std::shared_ptr<PrimitiveType>> GetPrimitiveType(const BoundTerm& term) {
+  if (!term.type()->is_primitive()) {
+    return InvalidExpression("Aggregate requires primitive type, got {}",
+                             term.type()->ToString());
+  }
+  return internal::checked_pointer_cast<PrimitiveType>(term.type());
+}
+
+class CountNonNullAggregator : public BoundAggregate::Aggregator {
+ public:
+  explicit CountNonNullAggregator(std::shared_ptr<BoundTerm> term)
+      : term_(std::move(term)) {}
+
+  Status Update(const StructLike& row) override {
+    ICEBERG_ASSIGN_OR_RAISE(auto literal, term_->Evaluate(row));
+    if (!literal.IsNull()) {
+      ++count_;
+    }
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override { return Literal::Long(count_); }
+
+ private:
+  std::shared_ptr<BoundTerm> term_;
+  int64_t count_ = 0;
+};
+
+class CountNullAggregator : public BoundAggregate::Aggregator {
+ public:
+  explicit CountNullAggregator(std::shared_ptr<BoundTerm> term)
+      : term_(std::move(term)) {}
+
+  Status Update(const StructLike& row) override {
+    ICEBERG_ASSIGN_OR_RAISE(auto literal, term_->Evaluate(row));
+    if (literal.IsNull()) {
+      ++count_;
+    }
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override { return Literal::Long(count_); }
+
+ private:
+  std::shared_ptr<BoundTerm> term_;
+  int64_t count_ = 0;
+};
+
+class CountStarAggregator : public BoundAggregate::Aggregator {
+ public:
+  Status Update(const StructLike& /*row*/) override {
+    ++count_;
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override { return Literal::Long(count_); }
+
+ private:
+  int64_t count_ = 0;
+};
+
+class ValueAggregatorImpl : public BoundAggregate::Aggregator {
+ public:
+  ValueAggregatorImpl(bool is_max, std::shared_ptr<BoundTerm> term)
+      : is_max_(is_max), term_(std::move(term)) {}
+
+  Status Update(const StructLike& row) override {
+    ICEBERG_ASSIGN_OR_RAISE(auto val_literal, term_->Evaluate(row));
+    if (val_literal.IsNull()) {
+      return {};
+    }
+    if (!current_) {
+      current_ = std::move(val_literal);
+      return {};
+    }
+
+    auto ordering = val_literal <=> *current_;
+    if (ordering == std::partial_ordering::unordered) {
+      return InvalidExpression("Cannot compare literals of type {}",
+                               val_literal.type()->ToString());
+    }
+
+    if (is_max_) {
+      if (ordering == std::partial_ordering::greater) {
+        current_ = std::move(val_literal);
+      }
+    } else {
+      if (ordering == std::partial_ordering::less) {
+        current_ = std::move(val_literal);
+      }
+    }
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override {
+    if (current_) {
+      return *current_;
+    }
+    ICEBERG_ASSIGN_OR_RAISE(auto type, GetPrimitiveType(*term_));
+    return Literal::Null(type);
+  }
+
+ private:
+  bool is_max_;
+  std::shared_ptr<BoundTerm> term_;
+  std::optional<Literal> current_;
+};
+
+}  // namespace
+
+// -------------------- Bound aggregates --------------------
+
+CountNonNullAggregate::CountNonNullAggregate(std::shared_ptr<BoundTerm> term)
+    : CountAggregate(Expression::Operation::kCount, std::move(term)) {}
+
+Result<std::shared_ptr<CountNonNullAggregate>> CountNonNullAggregate::Make(
+    std::shared_ptr<BoundTerm> term) {
+  if (!term) {
+    return InvalidExpression("Bound count aggregate requires non-null term");
+  }
+  return std::shared_ptr<CountNonNullAggregate>(
+      new CountNonNullAggregate(std::move(term)));
+}
+
+std::string CountNonNullAggregate::ToString() const {
+  ICEBERG_DCHECK(term() != nullptr, "Bound count aggregate should have term");
+  return std::format("count({})", term()->reference()->name());
+}
+
+Result<Literal> CountNonNullAggregate::Evaluate(const StructLike& data) const {
+  ICEBERG_ASSIGN_OR_RAISE(auto literal, term()->Evaluate(data));
+  return Literal::Long(literal.IsNull() ? 0 : 1);
+}
+
+Result<std::unique_ptr<BoundAggregate::Aggregator>> CountNonNullAggregate::NewAggregator()
+    const {
+  return std::unique_ptr<BoundAggregate::Aggregator>(new CountNonNullAggregator(term()));
+}
+
+CountNullAggregate::CountNullAggregate(std::shared_ptr<BoundTerm> term)
+    : CountAggregate(Expression::Operation::kCountNull, std::move(term)) {}
+
+Result<std::shared_ptr<CountNullAggregate>> CountNullAggregate::Make(
+    std::shared_ptr<BoundTerm> term) {
+  if (!term) {
+    return InvalidExpression("Bound count aggregate requires non-null term");
+  }
+  return std::shared_ptr<CountNullAggregate>(new CountNullAggregate(std::move(term)));
+}
+
+std::string CountNullAggregate::ToString() const {
+  ICEBERG_DCHECK(term() != nullptr, "Bound count aggregate should have term");
+  return std::format("count_null({})", term()->reference()->name());
+}
+
+Result<Literal> CountNullAggregate::Evaluate(const StructLike& data) const {
+  ICEBERG_ASSIGN_OR_RAISE(auto literal, term()->Evaluate(data));
+  return Literal::Long(literal.IsNull() ? 1 : 0);
+}
+
+Result<std::unique_ptr<BoundAggregate::Aggregator>> CountNullAggregate::NewAggregator()
+    const {
+  return std::unique_ptr<BoundAggregate::Aggregator>(new CountNullAggregator(term()));
+}
+
+CountStarAggregate::CountStarAggregate()
+    : CountAggregate(Expression::Operation::kCountStar, nullptr) {}
+
+Result<std::shared_ptr<CountStarAggregate>> CountStarAggregate::Make() {
+  return std::shared_ptr<CountStarAggregate>(new CountStarAggregate());
+}
+
+std::string CountStarAggregate::ToString() const { return "count(*)"; }
+
+Result<Literal> CountStarAggregate::Evaluate(const StructLike& data) const {
+  return Literal::Long(1);
+}
+
+Result<std::unique_ptr<BoundAggregate::Aggregator>> CountStarAggregate::NewAggregator()
+    const {
+  return std::unique_ptr<BoundAggregate::Aggregator>(new CountStarAggregator());
+}
+
+ValueAggregate::ValueAggregate(Expression::Operation op, std::shared_ptr<BoundTerm> term)
+    : BoundAggregate(op, std::move(term)) {}
+
+std::string ValueAggregate::ToString() const {
+  ICEBERG_DCHECK(term() != nullptr, "Bound value aggregate should have term");
+  auto prefix = op() == Expression::Operation::kMax ? "max" : "min";
+  return std::format("{}({})", prefix, term()->reference()->name());
+}
+
+Result<Literal> ValueAggregate::Evaluate(const StructLike& data) const {
+  ICEBERG_ASSIGN_OR_RAISE(auto literal, term()->Evaluate(data));
+  return literal;
+}
+
+Result<std::unique_ptr<BoundAggregate::Aggregator>> ValueAggregate::NewAggregator()
+    const {
+  bool is_max = op() == Expression::Operation::kMax;
+  return std::unique_ptr<BoundAggregate::Aggregator>(
+      new ValueAggregatorImpl(is_max, term()));
+}
+
+// -------------------- Unbound binding --------------------
+
+template <typename B>
+Result<std::shared_ptr<Expression>> UnboundAggregateImpl<B>::Bind(
+    const Schema& schema, bool case_sensitive) const {
+  ICEBERG_DCHECK(UnboundAggregateImpl<B>::IsSupportedOp(this->op()),
+                 "Unexpected aggregate operation");
+
+  std::shared_ptr<B> bound_term;
+  if (this->term()) {
+    ICEBERG_ASSIGN_OR_THROW(bound_term, this->term()->Bind(schema, case_sensitive));
+  }
+
+  switch (this->op()) {
+    case Expression::Operation::kCountStar: {
+      ICEBERG_ASSIGN_OR_THROW(auto aggregate, CountStarAggregate::Make());
+      return aggregate;
+    }
+    case Expression::Operation::kCount: {
+      if (!bound_term) {

Review Comment:
   Shouldn't this be checked by CountNonNullAggregate::Make?



##########
src/iceberg/expression/aggregate.h:
##########
@@ -0,0 +1,230 @@
+/*
+ * 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/aggregate.h
+/// Aggregate expression definitions.
+
+#include <memory>
+#include <optional>
+#include <string>
+#include <vector>
+
+#include "iceberg/expression/expression.h"
+#include "iceberg/expression/term.h"
+#include "iceberg/result.h"
+#include "iceberg/type_fwd.h"
+
+namespace iceberg {
+
+/// \brief Base aggregate holding an operation and a term.
+template <TermType T>
+class ICEBERG_EXPORT Aggregate : public virtual Expression {
+ public:
+  ~Aggregate() override = default;
+
+  Expression::Operation op() const override { return operation_; }
+
+  const std::shared_ptr<T>& term() const { return term_; }
+
+ protected:
+  Aggregate(Expression::Operation op, std::shared_ptr<T> term)
+      : operation_(op), term_(std::move(term)) {}
+
+  Expression::Operation operation_;
+  std::shared_ptr<T> term_;
+};
+
+/// \brief Base class for unbound aggregates.
+class ICEBERG_EXPORT UnboundAggregate : public virtual Expression,
+                                        public Unbound<Expression> {
+ public:
+  ~UnboundAggregate() override = default;
+
+  bool is_unbound_aggregate() const override { return true; }
+};
+
+/// \brief Template for unbound aggregates that carry a term and operation.
+template <typename B>
+class ICEBERG_EXPORT UnboundAggregateImpl : public UnboundAggregate,
+                                            public Aggregate<UnboundTerm<B>> {
+  using BASE = Aggregate<UnboundTerm<B>>;
+
+ public:
+  static Result<std::shared_ptr<UnboundAggregateImpl<B>>> Make(
+      Expression::Operation op, std::shared_ptr<UnboundTerm<B>> term);
+
+  std::shared_ptr<NamedReference> reference() override {
+    return BASE::term() ? BASE::term()->reference() : nullptr;
+  }
+
+  Result<std::shared_ptr<Expression>> Bind(const Schema& schema,
+                                           bool case_sensitive) const override;
+
+  std::string ToString() const override;
+
+ private:
+  static constexpr bool IsSupportedOp(Expression::Operation op) {
+    return op == Expression::Operation::kCount ||
+           op == Expression::Operation::kCountNull ||
+           op == Expression::Operation::kCountStar || op == Expression::Operation::kMax ||
+           op == Expression::Operation::kMin;
+  }
+
+  UnboundAggregateImpl(Expression::Operation op, std::shared_ptr<UnboundTerm<B>> term)
+      : BASE(op, std::move(term)) {
+    ICEBERG_DCHECK(IsSupportedOp(op), "Unexpected aggregate operation");
+    ICEBERG_DCHECK(op == Expression::Operation::kCountStar || BASE::term() != nullptr,
+                   "Aggregate term cannot be null unless COUNT(*)");
+  }
+};
+
+/// \brief Base class for bound aggregates.
+class ICEBERG_EXPORT BoundAggregate : public Aggregate<BoundTerm>, public Bound {
+ public:
+  using Aggregate<BoundTerm>::op;
+  using Aggregate<BoundTerm>::term;
+
+  class ICEBERG_EXPORT Aggregator {
+   public:
+    virtual ~Aggregator() = default;
+
+    virtual Status Update(const StructLike& row) = 0;
+    virtual Status Update(const DataFile& file) {
+      return NotSupported("Aggregating DataFile not supported");
+    }
+    virtual Result<Literal> ResultLiteral() const = 0;
+  };
+
+  std::shared_ptr<BoundReference> reference() override {
+    ICEBERG_DCHECK(term_ != nullptr || op() == Expression::Operation::kCountStar,
+                   "Bound aggregate term should not be null unless COUNT(*)");
+    return term_ ? term_->reference() : nullptr;
+  }
+
+  Result<Literal> Evaluate(const StructLike& data) const override = 0;
+
+  bool is_bound_aggregate() const override { return true; }
+
+  enum class Kind : int8_t {
+    // Count aggregates (COUNT, COUNT_STAR, COUNT_NULL)
+    kCount = 0,
+    // Value aggregates (MIN, MAX)
+    kValue,
+  };
+
+  virtual Kind kind() const = 0;
+  virtual Result<std::unique_ptr<Aggregator>> NewAggregator() const = 0;
+
+ protected:
+  BoundAggregate(Expression::Operation op, std::shared_ptr<BoundTerm> term)
+      : Aggregate<BoundTerm>(op, std::move(term)) {}
+};
+
+/// \brief Base class for COUNT aggregates.
+class ICEBERG_EXPORT CountAggregate : public BoundAggregate {
+ public:
+  Kind kind() const override { return Kind::kCount; }
+
+ protected:
+  CountAggregate(Expression::Operation op, std::shared_ptr<BoundTerm> term)
+      : BoundAggregate(op, std::move(term)) {}
+};
+
+/// \brief COUNT(term) aggregate.
+class ICEBERG_EXPORT CountNonNullAggregate : public CountAggregate {
+ public:
+  static Result<std::shared_ptr<CountNonNullAggregate>> Make(
+      std::shared_ptr<BoundTerm> term);
+
+  std::string ToString() const override;
+  Result<Literal> Evaluate(const StructLike& data) const override;
+  Result<std::unique_ptr<Aggregator>> NewAggregator() const override;
+
+ private:
+  explicit CountNonNullAggregate(std::shared_ptr<BoundTerm> term);
+};
+
+/// \brief COUNT_NULL(term) aggregate.
+class ICEBERG_EXPORT CountNullAggregate : public CountAggregate {
+ public:
+  static Result<std::shared_ptr<CountNullAggregate>> Make(
+      std::shared_ptr<BoundTerm> term);
+
+  std::string ToString() const override;
+  Result<Literal> Evaluate(const StructLike& data) const override;
+  Result<std::unique_ptr<Aggregator>> NewAggregator() const override;
+
+ private:
+  explicit CountNullAggregate(std::shared_ptr<BoundTerm> term);
+};
+
+/// \brief COUNT(*) aggregate.
+class ICEBERG_EXPORT CountStarAggregate : public CountAggregate {
+ public:
+  static Result<std::shared_ptr<CountStarAggregate>> Make();
+
+  std::string ToString() const override;
+  Result<Literal> Evaluate(const StructLike& data) const override;
+  Result<std::unique_ptr<Aggregator>> NewAggregator() const override;
+
+ private:
+  CountStarAggregate();
+};
+
+/// \brief Bound MAX/MIN aggregate.
+class ICEBERG_EXPORT ValueAggregate : public BoundAggregate {

Review Comment:
   Why not adding subclasses for MIN and MAX for ValueAggregate and ValueAggregator?



##########
src/iceberg/expression/expression_visitor.h:
##########
@@ -77,6 +79,22 @@ class ICEBERG_EXPORT ExpressionVisitor {
   /// \brief Visit an unbound predicate.
   /// \param pred The unbound predicate to visit
   virtual Result<R> Predicate(const std::shared_ptr<UnboundPredicate>& pred) = 0;
+
+  /// \brief Visit a bound aggregate.
+  /// \param aggregate The bound aggregate to visit.
+  virtual Result<R> Aggregate(const std::shared_ptr<BoundAggregate>& aggregate) {
+    ICEBERG_DCHECK(aggregate != nullptr, "Bound aggregate cannot be null");
+    return NotSupported("Visitor {} does not support bound aggregate: {}",
+                        typeid(*this).name(), aggregate->ToString());

Review Comment:
   ```suggestion
       return NotSupported("Visitor {} does not support bound aggregate",
                           typeid(*this).name());
   ```



##########
src/iceberg/expression/aggregate.h:
##########
@@ -0,0 +1,230 @@
+/*
+ * 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/aggregate.h
+/// Aggregate expression definitions.
+
+#include <memory>
+#include <optional>
+#include <string>
+#include <vector>
+
+#include "iceberg/expression/expression.h"
+#include "iceberg/expression/term.h"
+#include "iceberg/result.h"
+#include "iceberg/type_fwd.h"
+
+namespace iceberg {
+
+/// \brief Base aggregate holding an operation and a term.
+template <TermType T>
+class ICEBERG_EXPORT Aggregate : public virtual Expression {
+ public:
+  ~Aggregate() override = default;
+
+  Expression::Operation op() const override { return operation_; }
+
+  const std::shared_ptr<T>& term() const { return term_; }
+
+ protected:
+  Aggregate(Expression::Operation op, std::shared_ptr<T> term)
+      : operation_(op), term_(std::move(term)) {}
+
+  Expression::Operation operation_;
+  std::shared_ptr<T> term_;
+};
+
+/// \brief Base class for unbound aggregates.
+class ICEBERG_EXPORT UnboundAggregate : public virtual Expression,
+                                        public Unbound<Expression> {
+ public:
+  ~UnboundAggregate() override = default;
+
+  bool is_unbound_aggregate() const override { return true; }
+};
+
+/// \brief Template for unbound aggregates that carry a term and operation.
+template <typename B>
+class ICEBERG_EXPORT UnboundAggregateImpl : public UnboundAggregate,
+                                            public Aggregate<UnboundTerm<B>> {
+  using BASE = Aggregate<UnboundTerm<B>>;
+
+ public:
+  static Result<std::shared_ptr<UnboundAggregateImpl<B>>> Make(
+      Expression::Operation op, std::shared_ptr<UnboundTerm<B>> term);
+
+  std::shared_ptr<NamedReference> reference() override {
+    return BASE::term() ? BASE::term()->reference() : nullptr;
+  }
+
+  Result<std::shared_ptr<Expression>> Bind(const Schema& schema,
+                                           bool case_sensitive) const override;
+
+  std::string ToString() const override;
+
+ private:
+  static constexpr bool IsSupportedOp(Expression::Operation op) {
+    return op == Expression::Operation::kCount ||
+           op == Expression::Operation::kCountNull ||
+           op == Expression::Operation::kCountStar || op == Expression::Operation::kMax ||
+           op == Expression::Operation::kMin;
+  }
+
+  UnboundAggregateImpl(Expression::Operation op, std::shared_ptr<UnboundTerm<B>> term)
+      : BASE(op, std::move(term)) {
+    ICEBERG_DCHECK(IsSupportedOp(op), "Unexpected aggregate operation");
+    ICEBERG_DCHECK(op == Expression::Operation::kCountStar || BASE::term() != nullptr,
+                   "Aggregate term cannot be null unless COUNT(*)");

Review Comment:
   ```suggestion
                      "Aggregate term cannot be null except for COUNT(*)");
   ```



##########
src/iceberg/expression/aggregate.cc:
##########
@@ -0,0 +1,398 @@
+/*
+ * 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/aggregate.h"
+
+#include <format>
+#include <optional>
+#include <vector>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/type.h"
+#include "iceberg/util/checked_cast.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+Result<std::shared_ptr<PrimitiveType>> GetPrimitiveType(const BoundTerm& term) {
+  if (!term.type()->is_primitive()) {
+    return InvalidExpression("Aggregate requires primitive type, got {}",
+                             term.type()->ToString());
+  }
+  return internal::checked_pointer_cast<PrimitiveType>(term.type());
+}
+
+class CountNonNullAggregator : public BoundAggregate::Aggregator {
+ public:
+  explicit CountNonNullAggregator(std::shared_ptr<BoundTerm> term)
+      : term_(std::move(term)) {}
+
+  Status Update(const StructLike& row) override {
+    ICEBERG_ASSIGN_OR_RAISE(auto literal, term_->Evaluate(row));
+    if (!literal.IsNull()) {
+      ++count_;
+    }
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override { return Literal::Long(count_); }

Review Comment:
   Do we actually need to use `Result` to wrap the return type?



##########
src/iceberg/expression/aggregate.h:
##########
@@ -0,0 +1,230 @@
+/*
+ * 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/aggregate.h
+/// Aggregate expression definitions.
+
+#include <memory>
+#include <optional>
+#include <string>
+#include <vector>
+
+#include "iceberg/expression/expression.h"
+#include "iceberg/expression/term.h"
+#include "iceberg/result.h"
+#include "iceberg/type_fwd.h"
+
+namespace iceberg {
+
+/// \brief Base aggregate holding an operation and a term.
+template <TermType T>
+class ICEBERG_EXPORT Aggregate : public virtual Expression {
+ public:
+  ~Aggregate() override = default;
+
+  Expression::Operation op() const override { return operation_; }
+
+  const std::shared_ptr<T>& term() const { return term_; }
+
+ protected:
+  Aggregate(Expression::Operation op, std::shared_ptr<T> term)
+      : operation_(op), term_(std::move(term)) {}
+
+  Expression::Operation operation_;
+  std::shared_ptr<T> term_;
+};
+
+/// \brief Base class for unbound aggregates.
+class ICEBERG_EXPORT UnboundAggregate : public virtual Expression,
+                                        public Unbound<Expression> {
+ public:
+  ~UnboundAggregate() override = default;
+
+  bool is_unbound_aggregate() const override { return true; }
+};
+
+/// \brief Template for unbound aggregates that carry a term and operation.
+template <typename B>
+class ICEBERG_EXPORT UnboundAggregateImpl : public UnboundAggregate,
+                                            public Aggregate<UnboundTerm<B>> {
+  using BASE = Aggregate<UnboundTerm<B>>;
+
+ public:
+  static Result<std::shared_ptr<UnboundAggregateImpl<B>>> Make(
+      Expression::Operation op, std::shared_ptr<UnboundTerm<B>> term);
+
+  std::shared_ptr<NamedReference> reference() override {
+    return BASE::term() ? BASE::term()->reference() : nullptr;
+  }
+
+  Result<std::shared_ptr<Expression>> Bind(const Schema& schema,
+                                           bool case_sensitive) const override;
+
+  std::string ToString() const override;
+
+ private:
+  static constexpr bool IsSupportedOp(Expression::Operation op) {
+    return op == Expression::Operation::kCount ||
+           op == Expression::Operation::kCountNull ||
+           op == Expression::Operation::kCountStar || op == Expression::Operation::kMax ||
+           op == Expression::Operation::kMin;
+  }
+
+  UnboundAggregateImpl(Expression::Operation op, std::shared_ptr<UnboundTerm<B>> term)
+      : BASE(op, std::move(term)) {
+    ICEBERG_DCHECK(IsSupportedOp(op), "Unexpected aggregate operation");
+    ICEBERG_DCHECK(op == Expression::Operation::kCountStar || BASE::term() != nullptr,
+                   "Aggregate term cannot be null unless COUNT(*)");
+  }
+};
+
+/// \brief Base class for bound aggregates.
+class ICEBERG_EXPORT BoundAggregate : public Aggregate<BoundTerm>, public Bound {
+ public:
+  using Aggregate<BoundTerm>::op;
+  using Aggregate<BoundTerm>::term;
+
+  class ICEBERG_EXPORT Aggregator {
+   public:
+    virtual ~Aggregator() = default;
+
+    virtual Status Update(const StructLike& row) = 0;
+    virtual Status Update(const DataFile& file) {
+      return NotSupported("Aggregating DataFile not supported");
+    }
+    virtual Result<Literal> ResultLiteral() const = 0;
+  };
+
+  std::shared_ptr<BoundReference> reference() override {
+    ICEBERG_DCHECK(term_ != nullptr || op() == Expression::Operation::kCountStar,
+                   "Bound aggregate term should not be null unless COUNT(*)");

Review Comment:
   ```suggestion
                      "Bound aggregate term should not be null except for COUNT(*)");
   ```



##########
src/iceberg/expression/aggregate.cc:
##########
@@ -0,0 +1,398 @@
+/*
+ * 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/aggregate.h"
+
+#include <format>
+#include <optional>
+#include <vector>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/type.h"
+#include "iceberg/util/checked_cast.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+Result<std::shared_ptr<PrimitiveType>> GetPrimitiveType(const BoundTerm& term) {
+  if (!term.type()->is_primitive()) {
+    return InvalidExpression("Aggregate requires primitive type, got {}",
+                             term.type()->ToString());
+  }
+  return internal::checked_pointer_cast<PrimitiveType>(term.type());
+}
+
+class CountNonNullAggregator : public BoundAggregate::Aggregator {
+ public:
+  explicit CountNonNullAggregator(std::shared_ptr<BoundTerm> term)
+      : term_(std::move(term)) {}
+
+  Status Update(const StructLike& row) override {
+    ICEBERG_ASSIGN_OR_RAISE(auto literal, term_->Evaluate(row));
+    if (!literal.IsNull()) {
+      ++count_;
+    }
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override { return Literal::Long(count_); }
+
+ private:
+  std::shared_ptr<BoundTerm> term_;
+  int64_t count_ = 0;
+};
+
+class CountNullAggregator : public BoundAggregate::Aggregator {
+ public:
+  explicit CountNullAggregator(std::shared_ptr<BoundTerm> term)
+      : term_(std::move(term)) {}
+
+  Status Update(const StructLike& row) override {
+    ICEBERG_ASSIGN_OR_RAISE(auto literal, term_->Evaluate(row));
+    if (literal.IsNull()) {
+      ++count_;
+    }
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override { return Literal::Long(count_); }
+
+ private:
+  std::shared_ptr<BoundTerm> term_;
+  int64_t count_ = 0;
+};
+
+class CountStarAggregator : public BoundAggregate::Aggregator {
+ public:
+  Status Update(const StructLike& /*row*/) override {
+    ++count_;
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override { return Literal::Long(count_); }
+
+ private:
+  int64_t count_ = 0;
+};
+
+class ValueAggregatorImpl : public BoundAggregate::Aggregator {
+ public:
+  ValueAggregatorImpl(bool is_max, std::shared_ptr<BoundTerm> term)
+      : is_max_(is_max), term_(std::move(term)) {}
+
+  Status Update(const StructLike& row) override {
+    ICEBERG_ASSIGN_OR_RAISE(auto val_literal, term_->Evaluate(row));
+    if (val_literal.IsNull()) {
+      return {};
+    }
+    if (!current_) {
+      current_ = std::move(val_literal);
+      return {};
+    }
+
+    auto ordering = val_literal <=> *current_;
+    if (ordering == std::partial_ordering::unordered) {
+      return InvalidExpression("Cannot compare literals of type {}",
+                               val_literal.type()->ToString());
+    }
+
+    if (is_max_) {
+      if (ordering == std::partial_ordering::greater) {
+        current_ = std::move(val_literal);
+      }
+    } else {
+      if (ordering == std::partial_ordering::less) {
+        current_ = std::move(val_literal);
+      }
+    }
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override {
+    if (current_) {
+      return *current_;
+    }
+    ICEBERG_ASSIGN_OR_RAISE(auto type, GetPrimitiveType(*term_));
+    return Literal::Null(type);
+  }
+
+ private:
+  bool is_max_;
+  std::shared_ptr<BoundTerm> term_;
+  std::optional<Literal> current_;
+};
+
+}  // namespace
+
+// -------------------- Bound aggregates --------------------
+
+CountNonNullAggregate::CountNonNullAggregate(std::shared_ptr<BoundTerm> term)
+    : CountAggregate(Expression::Operation::kCount, std::move(term)) {}
+
+Result<std::shared_ptr<CountNonNullAggregate>> CountNonNullAggregate::Make(

Review Comment:
   Same for other Make functions.



##########
src/iceberg/expression/aggregate.cc:
##########
@@ -0,0 +1,398 @@
+/*
+ * 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/aggregate.h"
+
+#include <format>
+#include <optional>
+#include <vector>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/type.h"
+#include "iceberg/util/checked_cast.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+Result<std::shared_ptr<PrimitiveType>> GetPrimitiveType(const BoundTerm& term) {
+  if (!term.type()->is_primitive()) {
+    return InvalidExpression("Aggregate requires primitive type, got {}",
+                             term.type()->ToString());
+  }
+  return internal::checked_pointer_cast<PrimitiveType>(term.type());
+}
+
+class CountNonNullAggregator : public BoundAggregate::Aggregator {
+ public:
+  explicit CountNonNullAggregator(std::shared_ptr<BoundTerm> term)
+      : term_(std::move(term)) {}
+
+  Status Update(const StructLike& row) override {
+    ICEBERG_ASSIGN_OR_RAISE(auto literal, term_->Evaluate(row));
+    if (!literal.IsNull()) {
+      ++count_;
+    }
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override { return Literal::Long(count_); }
+
+ private:
+  std::shared_ptr<BoundTerm> term_;
+  int64_t count_ = 0;
+};
+
+class CountNullAggregator : public BoundAggregate::Aggregator {
+ public:
+  explicit CountNullAggregator(std::shared_ptr<BoundTerm> term)
+      : term_(std::move(term)) {}
+
+  Status Update(const StructLike& row) override {
+    ICEBERG_ASSIGN_OR_RAISE(auto literal, term_->Evaluate(row));
+    if (literal.IsNull()) {
+      ++count_;
+    }
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override { return Literal::Long(count_); }
+
+ private:
+  std::shared_ptr<BoundTerm> term_;
+  int64_t count_ = 0;
+};
+
+class CountStarAggregator : public BoundAggregate::Aggregator {
+ public:
+  Status Update(const StructLike& /*row*/) override {
+    ++count_;
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override { return Literal::Long(count_); }
+
+ private:
+  int64_t count_ = 0;
+};
+
+class ValueAggregatorImpl : public BoundAggregate::Aggregator {
+ public:
+  ValueAggregatorImpl(bool is_max, std::shared_ptr<BoundTerm> term)
+      : is_max_(is_max), term_(std::move(term)) {}
+
+  Status Update(const StructLike& row) override {
+    ICEBERG_ASSIGN_OR_RAISE(auto val_literal, term_->Evaluate(row));
+    if (val_literal.IsNull()) {
+      return {};
+    }
+    if (!current_) {
+      current_ = std::move(val_literal);
+      return {};
+    }
+
+    auto ordering = val_literal <=> *current_;
+    if (ordering == std::partial_ordering::unordered) {
+      return InvalidExpression("Cannot compare literals of type {}",
+                               val_literal.type()->ToString());
+    }
+
+    if (is_max_) {
+      if (ordering == std::partial_ordering::greater) {
+        current_ = std::move(val_literal);
+      }
+    } else {
+      if (ordering == std::partial_ordering::less) {
+        current_ = std::move(val_literal);
+      }
+    }
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override {
+    if (current_) {
+      return *current_;
+    }
+    ICEBERG_ASSIGN_OR_RAISE(auto type, GetPrimitiveType(*term_));
+    return Literal::Null(type);
+  }
+
+ private:
+  bool is_max_;
+  std::shared_ptr<BoundTerm> term_;
+  std::optional<Literal> current_;
+};
+
+}  // namespace
+
+// -------------------- Bound aggregates --------------------
+
+CountNonNullAggregate::CountNonNullAggregate(std::shared_ptr<BoundTerm> term)
+    : CountAggregate(Expression::Operation::kCount, std::move(term)) {}
+
+Result<std::shared_ptr<CountNonNullAggregate>> CountNonNullAggregate::Make(

Review Comment:
   ```suggestion
   Result<std::unique_ptr<CountNonNullAggregate>> CountNonNullAggregate::Make(
   ```



##########
src/iceberg/expression/aggregate.h:
##########
@@ -0,0 +1,230 @@
+/*
+ * 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/aggregate.h
+/// Aggregate expression definitions.
+
+#include <memory>
+#include <optional>
+#include <string>
+#include <vector>
+
+#include "iceberg/expression/expression.h"
+#include "iceberg/expression/term.h"
+#include "iceberg/result.h"
+#include "iceberg/type_fwd.h"
+
+namespace iceberg {
+
+/// \brief Base aggregate holding an operation and a term.
+template <TermType T>
+class ICEBERG_EXPORT Aggregate : public virtual Expression {
+ public:
+  ~Aggregate() override = default;
+
+  Expression::Operation op() const override { return operation_; }
+
+  const std::shared_ptr<T>& term() const { return term_; }
+
+ protected:
+  Aggregate(Expression::Operation op, std::shared_ptr<T> term)
+      : operation_(op), term_(std::move(term)) {}
+
+  Expression::Operation operation_;
+  std::shared_ptr<T> term_;
+};
+
+/// \brief Base class for unbound aggregates.
+class ICEBERG_EXPORT UnboundAggregate : public virtual Expression,
+                                        public Unbound<Expression> {
+ public:
+  ~UnboundAggregate() override = default;
+
+  bool is_unbound_aggregate() const override { return true; }
+};
+
+/// \brief Template for unbound aggregates that carry a term and operation.
+template <typename B>
+class ICEBERG_EXPORT UnboundAggregateImpl : public UnboundAggregate,
+                                            public Aggregate<UnboundTerm<B>> {
+  using BASE = Aggregate<UnboundTerm<B>>;
+
+ public:
+  static Result<std::shared_ptr<UnboundAggregateImpl<B>>> Make(
+      Expression::Operation op, std::shared_ptr<UnboundTerm<B>> term);
+
+  std::shared_ptr<NamedReference> reference() override {
+    return BASE::term() ? BASE::term()->reference() : nullptr;
+  }
+
+  Result<std::shared_ptr<Expression>> Bind(const Schema& schema,
+                                           bool case_sensitive) const override;
+
+  std::string ToString() const override;
+
+ private:
+  static constexpr bool IsSupportedOp(Expression::Operation op) {
+    return op == Expression::Operation::kCount ||
+           op == Expression::Operation::kCountNull ||
+           op == Expression::Operation::kCountStar || op == Expression::Operation::kMax ||
+           op == Expression::Operation::kMin;
+  }
+
+  UnboundAggregateImpl(Expression::Operation op, std::shared_ptr<UnboundTerm<B>> term)
+      : BASE(op, std::move(term)) {
+    ICEBERG_DCHECK(IsSupportedOp(op), "Unexpected aggregate operation");
+    ICEBERG_DCHECK(op == Expression::Operation::kCountStar || BASE::term() != nullptr,
+                   "Aggregate term cannot be null unless COUNT(*)");
+  }
+};
+
+/// \brief Base class for bound aggregates.
+class ICEBERG_EXPORT BoundAggregate : public Aggregate<BoundTerm>, public Bound {
+ public:
+  using Aggregate<BoundTerm>::op;
+  using Aggregate<BoundTerm>::term;
+
+  class ICEBERG_EXPORT Aggregator {
+   public:
+    virtual ~Aggregator() = default;
+
+    virtual Status Update(const StructLike& row) = 0;
+    virtual Status Update(const DataFile& file) {
+      return NotSupported("Aggregating DataFile not supported");
+    }
+    virtual Result<Literal> ResultLiteral() const = 0;
+  };
+
+  std::shared_ptr<BoundReference> reference() override {
+    ICEBERG_DCHECK(term_ != nullptr || op() == Expression::Operation::kCountStar,
+                   "Bound aggregate term should not be null unless COUNT(*)");
+    return term_ ? term_->reference() : nullptr;
+  }
+
+  Result<Literal> Evaluate(const StructLike& data) const override = 0;
+
+  bool is_bound_aggregate() const override { return true; }
+
+  enum class Kind : int8_t {
+    // Count aggregates (COUNT, COUNT_STAR, COUNT_NULL)
+    kCount = 0,
+    // Value aggregates (MIN, MAX)
+    kValue,
+  };
+
+  virtual Kind kind() const = 0;
+  virtual Result<std::unique_ptr<Aggregator>> NewAggregator() const = 0;
+
+ protected:
+  BoundAggregate(Expression::Operation op, std::shared_ptr<BoundTerm> term)
+      : Aggregate<BoundTerm>(op, std::move(term)) {}
+};
+
+/// \brief Base class for COUNT aggregates.
+class ICEBERG_EXPORT CountAggregate : public BoundAggregate {
+ public:
+  Kind kind() const override { return Kind::kCount; }
+
+ protected:
+  CountAggregate(Expression::Operation op, std::shared_ptr<BoundTerm> term)
+      : BoundAggregate(op, std::move(term)) {}
+};
+
+/// \brief COUNT(term) aggregate.
+class ICEBERG_EXPORT CountNonNullAggregate : public CountAggregate {
+ public:
+  static Result<std::shared_ptr<CountNonNullAggregate>> Make(
+      std::shared_ptr<BoundTerm> term);
+
+  std::string ToString() const override;
+  Result<Literal> Evaluate(const StructLike& data) const override;
+  Result<std::unique_ptr<Aggregator>> NewAggregator() const override;
+
+ private:
+  explicit CountNonNullAggregate(std::shared_ptr<BoundTerm> term);
+};
+
+/// \brief COUNT_NULL(term) aggregate.
+class ICEBERG_EXPORT CountNullAggregate : public CountAggregate {
+ public:
+  static Result<std::shared_ptr<CountNullAggregate>> Make(
+      std::shared_ptr<BoundTerm> term);
+
+  std::string ToString() const override;
+  Result<Literal> Evaluate(const StructLike& data) const override;
+  Result<std::unique_ptr<Aggregator>> NewAggregator() const override;
+
+ private:
+  explicit CountNullAggregate(std::shared_ptr<BoundTerm> term);
+};
+
+/// \brief COUNT(*) aggregate.
+class ICEBERG_EXPORT CountStarAggregate : public CountAggregate {
+ public:
+  static Result<std::shared_ptr<CountStarAggregate>> Make();
+
+  std::string ToString() const override;
+  Result<Literal> Evaluate(const StructLike& data) const override;
+  Result<std::unique_ptr<Aggregator>> NewAggregator() const override;
+
+ private:
+  CountStarAggregate();
+};
+
+/// \brief Bound MAX/MIN aggregate.
+class ICEBERG_EXPORT ValueAggregate : public BoundAggregate {
+ public:
+  ValueAggregate(Expression::Operation op, std::shared_ptr<BoundTerm> term);
+
+  Kind kind() const override { return Kind::kValue; }
+
+  std::string ToString() const override;
+  Result<Literal> Evaluate(const StructLike& data) const override;
+  Result<std::unique_ptr<Aggregator>> NewAggregator() const override;
+};
+
+/// \brief Evaluates bound aggregates over StructLike rows.
+class ICEBERG_EXPORT AggregateEvaluator {
+ public:
+  virtual ~AggregateEvaluator() = default;
+
+  /// \brief Create an evaluator for a single bound aggregate.
+  /// \param aggregate The bound aggregate to evaluate across rows.
+  static Result<std::unique_ptr<AggregateEvaluator>> Make(
+      std::shared_ptr<BoundAggregate> aggregate);
+
+  /// \brief Create an evaluator for multiple bound aggregates.
+  /// \param aggregates Aggregates to evaluate in one pass; order is preserved in
+  /// Results().
+  static Result<std::unique_ptr<AggregateEvaluator>> MakeList(

Review Comment:
   ```suggestion
     static Result<std::unique_ptr<AggregateEvaluator>> Make(
   ```



##########
src/iceberg/expression/aggregate.h:
##########
@@ -0,0 +1,230 @@
+/*
+ * 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/aggregate.h
+/// Aggregate expression definitions.
+
+#include <memory>
+#include <optional>
+#include <string>
+#include <vector>
+
+#include "iceberg/expression/expression.h"
+#include "iceberg/expression/term.h"
+#include "iceberg/result.h"
+#include "iceberg/type_fwd.h"
+
+namespace iceberg {
+
+/// \brief Base aggregate holding an operation and a term.
+template <TermType T>
+class ICEBERG_EXPORT Aggregate : public virtual Expression {
+ public:
+  ~Aggregate() override = default;
+
+  Expression::Operation op() const override { return operation_; }
+
+  const std::shared_ptr<T>& term() const { return term_; }
+
+ protected:
+  Aggregate(Expression::Operation op, std::shared_ptr<T> term)
+      : operation_(op), term_(std::move(term)) {}
+
+  Expression::Operation operation_;
+  std::shared_ptr<T> term_;
+};
+
+/// \brief Base class for unbound aggregates.
+class ICEBERG_EXPORT UnboundAggregate : public virtual Expression,
+                                        public Unbound<Expression> {
+ public:
+  ~UnboundAggregate() override = default;
+
+  bool is_unbound_aggregate() const override { return true; }
+};
+
+/// \brief Template for unbound aggregates that carry a term and operation.
+template <typename B>
+class ICEBERG_EXPORT UnboundAggregateImpl : public UnboundAggregate,
+                                            public Aggregate<UnboundTerm<B>> {
+  using BASE = Aggregate<UnboundTerm<B>>;
+
+ public:
+  static Result<std::shared_ptr<UnboundAggregateImpl<B>>> Make(
+      Expression::Operation op, std::shared_ptr<UnboundTerm<B>> term);
+
+  std::shared_ptr<NamedReference> reference() override {
+    return BASE::term() ? BASE::term()->reference() : nullptr;
+  }
+
+  Result<std::shared_ptr<Expression>> Bind(const Schema& schema,
+                                           bool case_sensitive) const override;
+
+  std::string ToString() const override;
+
+ private:
+  static constexpr bool IsSupportedOp(Expression::Operation op) {
+    return op == Expression::Operation::kCount ||
+           op == Expression::Operation::kCountNull ||
+           op == Expression::Operation::kCountStar || op == Expression::Operation::kMax ||
+           op == Expression::Operation::kMin;
+  }
+
+  UnboundAggregateImpl(Expression::Operation op, std::shared_ptr<UnboundTerm<B>> term)
+      : BASE(op, std::move(term)) {
+    ICEBERG_DCHECK(IsSupportedOp(op), "Unexpected aggregate operation");
+    ICEBERG_DCHECK(op == Expression::Operation::kCountStar || BASE::term() != nullptr,
+                   "Aggregate term cannot be null unless COUNT(*)");
+  }
+};
+
+/// \brief Base class for bound aggregates.
+class ICEBERG_EXPORT BoundAggregate : public Aggregate<BoundTerm>, public Bound {
+ public:
+  using Aggregate<BoundTerm>::op;
+  using Aggregate<BoundTerm>::term;
+
+  class ICEBERG_EXPORT Aggregator {
+   public:
+    virtual ~Aggregator() = default;
+
+    virtual Status Update(const StructLike& row) = 0;
+    virtual Status Update(const DataFile& file) {
+      return NotSupported("Aggregating DataFile not supported");
+    }
+    virtual Result<Literal> ResultLiteral() const = 0;
+  };
+
+  std::shared_ptr<BoundReference> reference() override {
+    ICEBERG_DCHECK(term_ != nullptr || op() == Expression::Operation::kCountStar,
+                   "Bound aggregate term should not be null unless COUNT(*)");
+    return term_ ? term_->reference() : nullptr;
+  }
+
+  Result<Literal> Evaluate(const StructLike& data) const override = 0;
+
+  bool is_bound_aggregate() const override { return true; }
+
+  enum class Kind : int8_t {
+    // Count aggregates (COUNT, COUNT_STAR, COUNT_NULL)
+    kCount = 0,
+    // Value aggregates (MIN, MAX)
+    kValue,
+  };
+
+  virtual Kind kind() const = 0;
+  virtual Result<std::unique_ptr<Aggregator>> NewAggregator() const = 0;

Review Comment:
   If it will not return any error, we can use `std::unique_ptr<Aggregator>` as its return type.



##########
src/iceberg/expression/aggregate.h:
##########
@@ -0,0 +1,230 @@
+/*
+ * 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/aggregate.h
+/// Aggregate expression definitions.
+
+#include <memory>
+#include <optional>
+#include <string>
+#include <vector>
+
+#include "iceberg/expression/expression.h"
+#include "iceberg/expression/term.h"
+#include "iceberg/result.h"
+#include "iceberg/type_fwd.h"
+
+namespace iceberg {
+
+/// \brief Base aggregate holding an operation and a term.
+template <TermType T>
+class ICEBERG_EXPORT Aggregate : public virtual Expression {
+ public:
+  ~Aggregate() override = default;
+
+  Expression::Operation op() const override { return operation_; }
+
+  const std::shared_ptr<T>& term() const { return term_; }
+
+ protected:
+  Aggregate(Expression::Operation op, std::shared_ptr<T> term)
+      : operation_(op), term_(std::move(term)) {}
+
+  Expression::Operation operation_;
+  std::shared_ptr<T> term_;
+};
+
+/// \brief Base class for unbound aggregates.
+class ICEBERG_EXPORT UnboundAggregate : public virtual Expression,
+                                        public Unbound<Expression> {
+ public:
+  ~UnboundAggregate() override = default;
+
+  bool is_unbound_aggregate() const override { return true; }
+};
+
+/// \brief Template for unbound aggregates that carry a term and operation.
+template <typename B>
+class ICEBERG_EXPORT UnboundAggregateImpl : public UnboundAggregate,
+                                            public Aggregate<UnboundTerm<B>> {
+  using BASE = Aggregate<UnboundTerm<B>>;
+
+ public:
+  static Result<std::shared_ptr<UnboundAggregateImpl<B>>> Make(
+      Expression::Operation op, std::shared_ptr<UnboundTerm<B>> term);
+
+  std::shared_ptr<NamedReference> reference() override {
+    return BASE::term() ? BASE::term()->reference() : nullptr;
+  }
+
+  Result<std::shared_ptr<Expression>> Bind(const Schema& schema,
+                                           bool case_sensitive) const override;
+
+  std::string ToString() const override;
+
+ private:
+  static constexpr bool IsSupportedOp(Expression::Operation op) {
+    return op == Expression::Operation::kCount ||
+           op == Expression::Operation::kCountNull ||
+           op == Expression::Operation::kCountStar || op == Expression::Operation::kMax ||
+           op == Expression::Operation::kMin;
+  }
+
+  UnboundAggregateImpl(Expression::Operation op, std::shared_ptr<UnboundTerm<B>> term)
+      : BASE(op, std::move(term)) {
+    ICEBERG_DCHECK(IsSupportedOp(op), "Unexpected aggregate operation");
+    ICEBERG_DCHECK(op == Expression::Operation::kCountStar || BASE::term() != nullptr,
+                   "Aggregate term cannot be null unless COUNT(*)");
+  }
+};
+
+/// \brief Base class for bound aggregates.
+class ICEBERG_EXPORT BoundAggregate : public Aggregate<BoundTerm>, public Bound {
+ public:
+  using Aggregate<BoundTerm>::op;
+  using Aggregate<BoundTerm>::term;
+
+  class ICEBERG_EXPORT Aggregator {
+   public:
+    virtual ~Aggregator() = default;
+
+    virtual Status Update(const StructLike& row) = 0;
+    virtual Status Update(const DataFile& file) {
+      return NotSupported("Aggregating DataFile not supported");
+    }
+    virtual Result<Literal> ResultLiteral() const = 0;
+  };
+
+  std::shared_ptr<BoundReference> reference() override {
+    ICEBERG_DCHECK(term_ != nullptr || op() == Expression::Operation::kCountStar,
+                   "Bound aggregate term should not be null unless COUNT(*)");
+    return term_ ? term_->reference() : nullptr;
+  }
+
+  Result<Literal> Evaluate(const StructLike& data) const override = 0;
+
+  bool is_bound_aggregate() const override { return true; }
+
+  enum class Kind : int8_t {
+    // Count aggregates (COUNT, COUNT_STAR, COUNT_NULL)
+    kCount = 0,
+    // Value aggregates (MIN, MAX)
+    kValue,
+  };
+
+  virtual Kind kind() const = 0;
+  virtual Result<std::unique_ptr<Aggregator>> NewAggregator() const = 0;
+
+ protected:
+  BoundAggregate(Expression::Operation op, std::shared_ptr<BoundTerm> term)
+      : Aggregate<BoundTerm>(op, std::move(term)) {}
+};
+
+/// \brief Base class for COUNT aggregates.
+class ICEBERG_EXPORT CountAggregate : public BoundAggregate {
+ public:
+  Kind kind() const override { return Kind::kCount; }
+
+ protected:
+  CountAggregate(Expression::Operation op, std::shared_ptr<BoundTerm> term)
+      : BoundAggregate(op, std::move(term)) {}
+};
+
+/// \brief COUNT(term) aggregate.
+class ICEBERG_EXPORT CountNonNullAggregate : public CountAggregate {
+ public:
+  static Result<std::shared_ptr<CountNonNullAggregate>> Make(
+      std::shared_ptr<BoundTerm> term);
+
+  std::string ToString() const override;
+  Result<Literal> Evaluate(const StructLike& data) const override;
+  Result<std::unique_ptr<Aggregator>> NewAggregator() const override;
+
+ private:
+  explicit CountNonNullAggregate(std::shared_ptr<BoundTerm> term);
+};
+
+/// \brief COUNT_NULL(term) aggregate.
+class ICEBERG_EXPORT CountNullAggregate : public CountAggregate {
+ public:
+  static Result<std::shared_ptr<CountNullAggregate>> Make(
+      std::shared_ptr<BoundTerm> term);
+
+  std::string ToString() const override;
+  Result<Literal> Evaluate(const StructLike& data) const override;
+  Result<std::unique_ptr<Aggregator>> NewAggregator() const override;
+
+ private:
+  explicit CountNullAggregate(std::shared_ptr<BoundTerm> term);
+};
+
+/// \brief COUNT(*) aggregate.
+class ICEBERG_EXPORT CountStarAggregate : public CountAggregate {
+ public:
+  static Result<std::shared_ptr<CountStarAggregate>> Make();
+
+  std::string ToString() const override;
+  Result<Literal> Evaluate(const StructLike& data) const override;
+  Result<std::unique_ptr<Aggregator>> NewAggregator() const override;
+
+ private:
+  CountStarAggregate();
+};
+
+/// \brief Bound MAX/MIN aggregate.
+class ICEBERG_EXPORT ValueAggregate : public BoundAggregate {
+ public:
+  ValueAggregate(Expression::Operation op, std::shared_ptr<BoundTerm> term);
+
+  Kind kind() const override { return Kind::kValue; }
+
+  std::string ToString() const override;
+  Result<Literal> Evaluate(const StructLike& data) const override;
+  Result<std::unique_ptr<Aggregator>> NewAggregator() const override;
+};
+
+/// \brief Evaluates bound aggregates over StructLike rows.
+class ICEBERG_EXPORT AggregateEvaluator {
+ public:
+  virtual ~AggregateEvaluator() = default;
+
+  /// \brief Create an evaluator for a single bound aggregate.
+  /// \param aggregate The bound aggregate to evaluate across rows.
+  static Result<std::unique_ptr<AggregateEvaluator>> Make(
+      std::shared_ptr<BoundAggregate> aggregate);
+
+  /// \brief Create an evaluator for multiple bound aggregates.
+  /// \param aggregates Aggregates to evaluate in one pass; order is preserved in
+  /// Results().
+  static Result<std::unique_ptr<AggregateEvaluator>> MakeList(
+      std::vector<std::shared_ptr<BoundAggregate>> aggregates);
+
+  /// \brief Update aggregates with a row.
+  virtual Status Update(const StructLike& row) = 0;
+
+  /// \brief Final aggregated value.
+  virtual Result<std::vector<Literal>> Results() const = 0;
+
+  /// \brief Convenience accessor when only one aggregate is evaluated.
+  virtual Result<Literal> ResultLiteral() const = 0;

Review Comment:
   ```suggestion
     virtual Result<std::vector<Literal>> GetResults() const = 0;
   
     /// \brief Convenience accessor when only one aggregate is evaluated.
     virtual Result<Literal> GetResult() const = 0;
   ```
   
   Perhaps this looks more consistent.



##########
src/iceberg/expression/expression_visitor.h:
##########
@@ -77,6 +79,22 @@ class ICEBERG_EXPORT ExpressionVisitor {
   /// \brief Visit an unbound predicate.
   /// \param pred The unbound predicate to visit
   virtual Result<R> Predicate(const std::shared_ptr<UnboundPredicate>& pred) = 0;
+
+  /// \brief Visit a bound aggregate.
+  /// \param aggregate The bound aggregate to visit.
+  virtual Result<R> Aggregate(const std::shared_ptr<BoundAggregate>& aggregate) {
+    ICEBERG_DCHECK(aggregate != nullptr, "Bound aggregate cannot be null");
+    return NotSupported("Visitor {} does not support bound aggregate: {}",
+                        typeid(*this).name(), aggregate->ToString());
+  }
+
+  /// \brief Visit an unbound aggregate.
+  /// \param aggregate The unbound aggregate to visit.
+  virtual Result<R> Aggregate(const std::shared_ptr<UnboundAggregate>& aggregate) {
+    ICEBERG_DCHECK(aggregate != nullptr, "Unbound aggregate cannot be null");
+    return NotSupported("Visitor {} does not support unbound aggregate: {}",
+                        typeid(*this).name(), aggregate->ToString());

Review Comment:
   ```suggestion
       ICEBERG_DCHECK(aggregate != nullptr, "Unbound aggregate cannot be null");
       return NotSupported("Visitor {} does not support unbound aggregate",
                           typeid(*this).name());
   ```



##########
src/iceberg/expression/aggregate.h:
##########
@@ -0,0 +1,230 @@
+/*
+ * 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/aggregate.h
+/// Aggregate expression definitions.
+
+#include <memory>
+#include <optional>
+#include <string>
+#include <vector>
+
+#include "iceberg/expression/expression.h"
+#include "iceberg/expression/term.h"
+#include "iceberg/result.h"
+#include "iceberg/type_fwd.h"
+
+namespace iceberg {
+
+/// \brief Base aggregate holding an operation and a term.
+template <TermType T>
+class ICEBERG_EXPORT Aggregate : public virtual Expression {
+ public:
+  ~Aggregate() override = default;
+
+  Expression::Operation op() const override { return operation_; }
+
+  const std::shared_ptr<T>& term() const { return term_; }
+
+ protected:
+  Aggregate(Expression::Operation op, std::shared_ptr<T> term)
+      : operation_(op), term_(std::move(term)) {}
+
+  Expression::Operation operation_;
+  std::shared_ptr<T> term_;
+};
+
+/// \brief Base class for unbound aggregates.
+class ICEBERG_EXPORT UnboundAggregate : public virtual Expression,
+                                        public Unbound<Expression> {
+ public:
+  ~UnboundAggregate() override = default;
+
+  bool is_unbound_aggregate() const override { return true; }
+};
+
+/// \brief Template for unbound aggregates that carry a term and operation.
+template <typename B>
+class ICEBERG_EXPORT UnboundAggregateImpl : public UnboundAggregate,
+                                            public Aggregate<UnboundTerm<B>> {
+  using BASE = Aggregate<UnboundTerm<B>>;
+
+ public:
+  static Result<std::shared_ptr<UnboundAggregateImpl<B>>> Make(
+      Expression::Operation op, std::shared_ptr<UnboundTerm<B>> term);
+
+  std::shared_ptr<NamedReference> reference() override {
+    return BASE::term() ? BASE::term()->reference() : nullptr;
+  }
+
+  Result<std::shared_ptr<Expression>> Bind(const Schema& schema,
+                                           bool case_sensitive) const override;
+
+  std::string ToString() const override;
+
+ private:
+  static constexpr bool IsSupportedOp(Expression::Operation op) {
+    return op == Expression::Operation::kCount ||
+           op == Expression::Operation::kCountNull ||
+           op == Expression::Operation::kCountStar || op == Expression::Operation::kMax ||
+           op == Expression::Operation::kMin;
+  }
+
+  UnboundAggregateImpl(Expression::Operation op, std::shared_ptr<UnboundTerm<B>> term)
+      : BASE(op, std::move(term)) {
+    ICEBERG_DCHECK(IsSupportedOp(op), "Unexpected aggregate operation");
+    ICEBERG_DCHECK(op == Expression::Operation::kCountStar || BASE::term() != nullptr,
+                   "Aggregate term cannot be null unless COUNT(*)");
+  }
+};
+
+/// \brief Base class for bound aggregates.
+class ICEBERG_EXPORT BoundAggregate : public Aggregate<BoundTerm>, public Bound {
+ public:
+  using Aggregate<BoundTerm>::op;
+  using Aggregate<BoundTerm>::term;
+
+  class ICEBERG_EXPORT Aggregator {

Review Comment:
   ```suggestion
     class Aggregator {
   ```
   
   I think we don't need to add export for inner class.



##########
src/iceberg/expression/aggregate.h:
##########
@@ -0,0 +1,230 @@
+/*
+ * 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/aggregate.h
+/// Aggregate expression definitions.
+
+#include <memory>
+#include <optional>
+#include <string>
+#include <vector>
+
+#include "iceberg/expression/expression.h"
+#include "iceberg/expression/term.h"
+#include "iceberg/result.h"
+#include "iceberg/type_fwd.h"
+
+namespace iceberg {
+
+/// \brief Base aggregate holding an operation and a term.
+template <TermType T>
+class ICEBERG_EXPORT Aggregate : public virtual Expression {
+ public:
+  ~Aggregate() override = default;
+
+  Expression::Operation op() const override { return operation_; }
+
+  const std::shared_ptr<T>& term() const { return term_; }
+
+ protected:
+  Aggregate(Expression::Operation op, std::shared_ptr<T> term)
+      : operation_(op), term_(std::move(term)) {}
+
+  Expression::Operation operation_;
+  std::shared_ptr<T> term_;
+};
+
+/// \brief Base class for unbound aggregates.
+class ICEBERG_EXPORT UnboundAggregate : public virtual Expression,
+                                        public Unbound<Expression> {
+ public:
+  ~UnboundAggregate() override = default;
+
+  bool is_unbound_aggregate() const override { return true; }
+};
+
+/// \brief Template for unbound aggregates that carry a term and operation.
+template <typename B>
+class ICEBERG_EXPORT UnboundAggregateImpl : public UnboundAggregate,
+                                            public Aggregate<UnboundTerm<B>> {
+  using BASE = Aggregate<UnboundTerm<B>>;
+
+ public:
+  static Result<std::shared_ptr<UnboundAggregateImpl<B>>> Make(
+      Expression::Operation op, std::shared_ptr<UnboundTerm<B>> term);
+
+  std::shared_ptr<NamedReference> reference() override {
+    return BASE::term() ? BASE::term()->reference() : nullptr;
+  }
+
+  Result<std::shared_ptr<Expression>> Bind(const Schema& schema,
+                                           bool case_sensitive) const override;
+
+  std::string ToString() const override;
+
+ private:
+  static constexpr bool IsSupportedOp(Expression::Operation op) {
+    return op == Expression::Operation::kCount ||
+           op == Expression::Operation::kCountNull ||
+           op == Expression::Operation::kCountStar || op == Expression::Operation::kMax ||
+           op == Expression::Operation::kMin;
+  }
+
+  UnboundAggregateImpl(Expression::Operation op, std::shared_ptr<UnboundTerm<B>> term)
+      : BASE(op, std::move(term)) {
+    ICEBERG_DCHECK(IsSupportedOp(op), "Unexpected aggregate operation");
+    ICEBERG_DCHECK(op == Expression::Operation::kCountStar || BASE::term() != nullptr,
+                   "Aggregate term cannot be null unless COUNT(*)");
+  }
+};
+
+/// \brief Base class for bound aggregates.
+class ICEBERG_EXPORT BoundAggregate : public Aggregate<BoundTerm>, public Bound {
+ public:
+  using Aggregate<BoundTerm>::op;
+  using Aggregate<BoundTerm>::term;
+
+  class ICEBERG_EXPORT Aggregator {
+   public:
+    virtual ~Aggregator() = default;
+
+    virtual Status Update(const StructLike& row) = 0;
+    virtual Status Update(const DataFile& file) {
+      return NotSupported("Aggregating DataFile not supported");
+    }
+    virtual Result<Literal> ResultLiteral() const = 0;

Review Comment:
   ```suggestion
       virtual Result<Literal> GetResult() const = 0;
   ```



-- 
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: issues-unsubscribe@iceberg.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org


Re: [PR] feat: add aggregate expressions and evaluator [iceberg-cpp]

Posted by "SuKi2cn (via GitHub)" <gi...@apache.org>.
SuKi2cn commented on PR #335:
URL: https://github.com/apache/iceberg-cpp/pull/335#issuecomment-3562361844

   I've updated the design to align with the current Predicate pattern and the Java implementation:
   - Removed unbound aggregate subclasses (e.g. CountAggregate on the unbound side).
   - Introduced UnboundAggregateImpl<BoundReference> to handle all unbound aggregates, similar to Predicate.
   - Refactored BoundAggregate to be the primary execution unit and moved row-level logic into its subclasses.
   - Updated AggregateEvaluator to act as a driver over bound aggregates instead of owning aggregate semantics.
   


-- 
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: issues-unsubscribe@iceberg.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org


Re: [PR] feat: add aggregate expressions and evaluator [iceberg-cpp]

Posted by "SuKi2cn (via GitHub)" <gi...@apache.org>.
SuKi2cn commented on code in PR #335:
URL: https://github.com/apache/iceberg-cpp/pull/335#discussion_r2553854805


##########
src/iceberg/expression/aggregate.cc:
##########
@@ -0,0 +1,289 @@
+/*
+ * 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/aggregate.h"
+
+#include <format>
+#include <optional>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/expression/expression.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/type.h"
+#include "iceberg/util/checked_cast.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+std::string OperationToPrefix(Expression::Operation op) {

Review Comment:
   > Why don't we use `std::string_view` instead of `std::string` if the return values should be constant string literals?
   
   Good point — these return values are all string literals with static lifetime, so `std::string_view` is more appropriate here. I'll update the signature accordingly.
   



-- 
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: issues-unsubscribe@iceberg.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org


Re: [PR] feat: add aggregate expressions and evaluator [iceberg-cpp]

Posted by "Jinchul81 (via GitHub)" <gi...@apache.org>.
Jinchul81 commented on code in PR #335:
URL: https://github.com/apache/iceberg-cpp/pull/335#discussion_r2553994629


##########
src/iceberg/expression/aggregate.cc:
##########
@@ -0,0 +1,289 @@
+/*
+ * 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/aggregate.h"
+
+#include <format>
+#include <optional>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/expression/expression.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/type.h"
+#include "iceberg/util/checked_cast.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+std::string OperationToPrefix(Expression::Operation op) {
+  switch (op) {
+    case Expression::Operation::kMax:
+      return "max";
+    case Expression::Operation::kMin:
+      return "min";
+    case Expression::Operation::kCount:
+    case Expression::Operation::kCountStar:
+      return "count";
+    default:
+      break;
+  }
+  return "aggregate";
+}
+
+Result<std::shared_ptr<PrimitiveType>> GetPrimitiveType(const BoundTerm& term) {
+  auto primitive = std::dynamic_pointer_cast<PrimitiveType>(term.type());
+  if (primitive == nullptr) {
+    return InvalidExpression("Aggregate requires primitive type, got {}",
+                             term.type()->ToString());
+  }
+  return primitive;
+}
+
+}  // namespace
+
+CountAggregate::CountAggregate(Expression::Operation op, Mode mode,
+                               std::shared_ptr<UnboundTerm<BoundReference>> term,
+                               std::shared_ptr<NamedReference> reference)
+    : UnboundAggregate(op),
+      mode_(mode),
+      term_(std::move(term)),
+      reference_(std::move(reference)) {}
+
+Result<std::unique_ptr<CountAggregate>> CountAggregate::Count(
+    std::shared_ptr<UnboundTerm<BoundReference>> term) {
+  auto ref = term->reference();
+  return std::unique_ptr<CountAggregate>(new CountAggregate(
+      Expression::Operation::kCount, Mode::kNonNull, std::move(term), std::move(ref)));
+}
+
+Result<std::unique_ptr<CountAggregate>> CountAggregate::CountNull(
+    std::shared_ptr<UnboundTerm<BoundReference>> term) {
+  auto ref = term->reference();
+  return std::unique_ptr<CountAggregate>(new CountAggregate(
+      Expression::Operation::kCount, Mode::kNull, std::move(term), std::move(ref)));
+}
+
+std::unique_ptr<CountAggregate> CountAggregate::CountStar() {
+  return std::unique_ptr<CountAggregate>(new CountAggregate(
+      Expression::Operation::kCountStar, Mode::kStar, nullptr, nullptr));
+}
+
+std::string CountAggregate::ToString() const {
+  if (mode_ == Mode::kStar) {
+    return "count(*)";
+  }
+  ICEBERG_DCHECK(reference_ != nullptr, "Count aggregate should have reference");
+  switch (mode_) {
+    case Mode::kNull:
+      return std::format("count_null({})", reference_->name());
+    case Mode::kNonNull:
+      return std::format("count({})", reference_->name());
+    case Mode::kStar:
+      break;
+  }
+  std::unreachable();
+}
+
+Result<std::shared_ptr<Expression>> CountAggregate::Bind(const Schema& schema,
+                                                         bool case_sensitive) const {
+  std::shared_ptr<BoundTerm> bound_term;
+  if (term_ != nullptr) {
+    ICEBERG_ASSIGN_OR_THROW(auto bound, term_->Bind(schema, case_sensitive));
+    bound_term = std::move(bound);
+  }
+  auto aggregate =
+      std::make_shared<BoundCountAggregate>(op(), mode_, std::move(bound_term));
+  return aggregate;
+}
+
+BoundCountAggregate::BoundCountAggregate(Expression::Operation op,
+                                         CountAggregate::Mode mode,
+                                         std::shared_ptr<BoundTerm> term)
+    : BoundAggregate(op, std::move(term)), mode_(mode) {}
+
+std::string BoundCountAggregate::ToString() const {
+  if (mode_ == CountAggregate::Mode::kStar) {
+    return "count(*)";
+  }
+  ICEBERG_DCHECK(term() != nullptr, "Bound count aggregate should have term");
+  switch (mode_) {
+    case CountAggregate::Mode::kNull:
+      return std::format("count_null({})", term()->reference()->name());
+    case CountAggregate::Mode::kNonNull:
+      return std::format("count({})", term()->reference()->name());
+    case CountAggregate::Mode::kStar:
+      break;
+  }
+  std::unreachable();
+}
+
+ValueAggregate::ValueAggregate(Expression::Operation op,
+                               std::shared_ptr<UnboundTerm<BoundReference>> term,
+                               std::shared_ptr<NamedReference> reference)
+    : UnboundAggregate(op), term_(std::move(term)), reference_(std::move(reference)) {}
+
+Result<std::unique_ptr<ValueAggregate>> ValueAggregate::Max(
+    std::shared_ptr<UnboundTerm<BoundReference>> term) {
+  auto ref = term->reference();
+  return std::unique_ptr<ValueAggregate>(
+      new ValueAggregate(Expression::Operation::kMax, std::move(term), std::move(ref)));
+}
+
+Result<std::unique_ptr<ValueAggregate>> ValueAggregate::Min(
+    std::shared_ptr<UnboundTerm<BoundReference>> term) {
+  auto ref = term->reference();
+  return std::unique_ptr<ValueAggregate>(
+      new ValueAggregate(Expression::Operation::kMin, std::move(term), std::move(ref)));
+}
+
+std::string ValueAggregate::ToString() const {
+  return std::format("{}({})", OperationToPrefix(op()), reference_->name());
+}
+
+Result<std::shared_ptr<Expression>> ValueAggregate::Bind(const Schema& schema,
+                                                         bool case_sensitive) const {
+  ICEBERG_ASSIGN_OR_THROW(auto bound, term_->Bind(schema, case_sensitive));
+  auto aggregate = std::make_shared<BoundValueAggregate>(
+      op(), std::shared_ptr<BoundTerm>(std::move(bound)));
+  return aggregate;
+}
+
+BoundValueAggregate::BoundValueAggregate(Expression::Operation op,
+                                         std::shared_ptr<BoundTerm> term)
+    : BoundAggregate(op, std::move(term)) {}
+
+std::string BoundValueAggregate::ToString() const {
+  ICEBERG_DCHECK(term() != nullptr, "Bound value aggregate should have term");
+  return std::format("{}({})", OperationToPrefix(op()), term()->reference()->name());
+}
+
+namespace {
+
+class CountEvaluator : public AggregateEvaluator {
+ public:
+  CountEvaluator(CountAggregate::Mode mode, std::shared_ptr<BoundTerm> term)
+      : mode_(mode), term_(std::move(term)) {}
+
+  Status Add(const StructLike& row) override {
+    switch (mode_) {
+      case CountAggregate::Mode::kStar:
+        ++count_;
+        return {};
+      case CountAggregate::Mode::kNonNull: {
+        ICEBERG_ASSIGN_OR_RAISE(auto literal, term_->Evaluate(row));
+        if (!literal.IsNull()) {
+          ++count_;
+        }
+        return {};
+      }
+      case CountAggregate::Mode::kNull: {
+        ICEBERG_ASSIGN_OR_RAISE(auto literal, term_->Evaluate(row));
+        if (literal.IsNull()) {
+          ++count_;
+        }
+        return {};
+      }
+    }
+    std::unreachable();
+  }
+
+  Result<Literal> ResultLiteral() const override { return Literal::Long(count_); }
+
+ private:
+  CountAggregate::Mode mode_;
+  std::shared_ptr<BoundTerm> term_;
+  int64_t count_ = 0;
+};
+
+class ValueAggregateEvaluator : public AggregateEvaluator {
+ public:
+  ValueAggregateEvaluator(Expression::Operation op, std::shared_ptr<BoundTerm> term,
+                          std::shared_ptr<PrimitiveType> type)
+      : op_(op), term_(std::move(term)), type_(std::move(type)) {}
+
+  Status Add(const StructLike& row) override {
+    ICEBERG_ASSIGN_OR_RAISE(auto literal, term_->Evaluate(row));
+    if (literal.IsNull()) {
+      return {};
+    }
+
+    if (!current_) {
+      current_ = std::move(literal);
+      return {};
+    }
+
+    auto ordering = literal <=> *current_;
+    if (ordering == std::partial_ordering::unordered) {
+      return InvalidExpression("Cannot compare literals of type {}",
+                               literal.type()->ToString());
+    }
+
+    if (op_ == Expression::Operation::kMax) {
+      if (ordering == std::partial_ordering::greater) {
+        current_ = std::move(literal);
+      }
+    } else {
+      if (ordering == std::partial_ordering::less) {
+        current_ = std::move(literal);
+      }
+    }
+    return {};
+  }
+
+  Result<Literal> ResultLiteral() const override {
+    if (!current_) {
+      return Literal::Null(type_);
+    }
+    return *current_;
+  }
+
+ private:
+  Expression::Operation op_;
+  std::shared_ptr<BoundTerm> term_;
+  std::shared_ptr<PrimitiveType> type_;
+  std::optional<Literal> current_;
+};
+
+}  // namespace
+
+Result<std::unique_ptr<AggregateEvaluator>> AggregateEvaluator::Make(
+    std::shared_ptr<BoundAggregate> aggregate) {
+  ICEBERG_DCHECK(aggregate != nullptr, "Aggregate cannot be null");
+
+  if (auto count = std::dynamic_pointer_cast<BoundCountAggregate>(aggregate)) {
+    if (count->mode() != CountAggregate::Mode::kStar && !count->term()) {

Review Comment:
   Please write the code like `if (count->mode() != CountAggregate::Mode::kStar && !count->term()) [[unlikely]] {` to help branch prediction.
   



-- 
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: issues-unsubscribe@iceberg.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org


Re: [PR] feat: add aggregate expressions and evaluator [iceberg-cpp]

Posted by "Jinchul81 (via GitHub)" <gi...@apache.org>.
Jinchul81 commented on code in PR #335:
URL: https://github.com/apache/iceberg-cpp/pull/335#discussion_r2553994059


##########
src/iceberg/expression/aggregate.cc:
##########
@@ -0,0 +1,289 @@
+/*
+ * 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/aggregate.h"
+
+#include <format>
+#include <optional>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/expression/expression.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/type.h"
+#include "iceberg/util/checked_cast.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+std::string OperationToPrefix(Expression::Operation op) {
+  switch (op) {
+    case Expression::Operation::kMax:
+      return "max";
+    case Expression::Operation::kMin:
+      return "min";
+    case Expression::Operation::kCount:
+    case Expression::Operation::kCountStar:
+      return "count";
+    default:
+      break;
+  }
+  return "aggregate";
+}
+
+Result<std::shared_ptr<PrimitiveType>> GetPrimitiveType(const BoundTerm& term) {
+  auto primitive = std::dynamic_pointer_cast<PrimitiveType>(term.type());
+  if (primitive == nullptr) {
+    return InvalidExpression("Aggregate requires primitive type, got {}",
+                             term.type()->ToString());
+  }
+  return primitive;
+}
+
+}  // namespace
+
+CountAggregate::CountAggregate(Expression::Operation op, Mode mode,
+                               std::shared_ptr<UnboundTerm<BoundReference>> term,
+                               std::shared_ptr<NamedReference> reference)
+    : UnboundAggregate(op),
+      mode_(mode),
+      term_(std::move(term)),
+      reference_(std::move(reference)) {}
+
+Result<std::unique_ptr<CountAggregate>> CountAggregate::Count(
+    std::shared_ptr<UnboundTerm<BoundReference>> term) {
+  auto ref = term->reference();
+  return std::unique_ptr<CountAggregate>(new CountAggregate(
+      Expression::Operation::kCount, Mode::kNonNull, std::move(term), std::move(ref)));
+}
+
+Result<std::unique_ptr<CountAggregate>> CountAggregate::CountNull(
+    std::shared_ptr<UnboundTerm<BoundReference>> term) {
+  auto ref = term->reference();
+  return std::unique_ptr<CountAggregate>(new CountAggregate(
+      Expression::Operation::kCount, Mode::kNull, std::move(term), std::move(ref)));
+}
+
+std::unique_ptr<CountAggregate> CountAggregate::CountStar() {
+  return std::unique_ptr<CountAggregate>(new CountAggregate(
+      Expression::Operation::kCountStar, Mode::kStar, nullptr, nullptr));
+}
+
+std::string CountAggregate::ToString() const {
+  if (mode_ == Mode::kStar) {
+    return "count(*)";
+  }
+  ICEBERG_DCHECK(reference_ != nullptr, "Count aggregate should have reference");
+  switch (mode_) {
+    case Mode::kNull:
+      return std::format("count_null({})", reference_->name());
+    case Mode::kNonNull:
+      return std::format("count({})", reference_->name());
+    case Mode::kStar:
+      break;
+  }
+  std::unreachable();
+}
+
+Result<std::shared_ptr<Expression>> CountAggregate::Bind(const Schema& schema,
+                                                         bool case_sensitive) const {
+  std::shared_ptr<BoundTerm> bound_term;
+  if (term_ != nullptr) {

Review Comment:
   Please write the code like `if (term_ != nullptr) [[unlikely]] {` to help branch prediction.



-- 
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: issues-unsubscribe@iceberg.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org


Re: [PR] feat: add aggregate expressions and evaluator [iceberg-cpp]

Posted by "wgtmac (via GitHub)" <gi...@apache.org>.
wgtmac commented on code in PR #335:
URL: https://github.com/apache/iceberg-cpp/pull/335#discussion_r2549861180


##########
src/iceberg/expression/aggregate.h:
##########
@@ -0,0 +1,175 @@
+/*
+ * 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/aggregate.h
+/// Aggregate expression definitions.
+
+#include <concepts>
+#include <memory>
+#include <vector>
+
+#include "iceberg/expression/expression.h"
+#include "iceberg/expression/term.h"
+#include "iceberg/result.h"
+
+namespace iceberg {
+
+template <typename T>
+concept AggregateTermType = std::derived_from<T, Term>;
+
+/// \brief Base aggregate holding an operation and a term.
+template <AggregateTermType T>
+class ICEBERG_EXPORT Aggregate : public virtual Expression {
+ public:
+  ~Aggregate() override = default;
+
+  Expression::Operation op() const override { return operation_; }
+
+  const std::shared_ptr<T>& term() const { return term_; }
+

Review Comment:
   Please also add a `std::string ToString() const` similar to the Java impl.



##########
src/iceberg/expression/aggregate.h:
##########
@@ -0,0 +1,175 @@
+/*
+ * 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/aggregate.h
+/// Aggregate expression definitions.
+
+#include <concepts>
+#include <memory>
+#include <vector>
+
+#include "iceberg/expression/expression.h"
+#include "iceberg/expression/term.h"
+#include "iceberg/result.h"
+
+namespace iceberg {
+
+template <typename T>
+concept AggregateTermType = std::derived_from<T, Term>;
+
+/// \brief Base aggregate holding an operation and a term.
+template <AggregateTermType T>
+class ICEBERG_EXPORT Aggregate : public virtual Expression {
+ public:
+  ~Aggregate() override = default;
+
+  Expression::Operation op() const override { return operation_; }
+
+  const std::shared_ptr<T>& term() const { return term_; }
+
+ protected:
+  Aggregate(Expression::Operation op, std::shared_ptr<T> term)
+      : operation_(op), term_(std::move(term)) {}

Review Comment:
   nit: add `ICEBERG_DCHECK(term != nullptr)` when `op` is not count star.



##########
src/iceberg/expression/aggregate.h:
##########
@@ -0,0 +1,175 @@
+/*
+ * 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/aggregate.h
+/// Aggregate expression definitions.
+
+#include <concepts>
+#include <memory>
+#include <vector>
+
+#include "iceberg/expression/expression.h"
+#include "iceberg/expression/term.h"
+#include "iceberg/result.h"
+
+namespace iceberg {
+
+template <typename T>
+concept AggregateTermType = std::derived_from<T, Term>;
+
+/// \brief Base aggregate holding an operation and a term.
+template <AggregateTermType T>
+class ICEBERG_EXPORT Aggregate : public virtual Expression {
+ public:
+  ~Aggregate() override = default;
+
+  Expression::Operation op() const override { return operation_; }
+
+  const std::shared_ptr<T>& term() const { return term_; }
+
+ protected:
+  Aggregate(Expression::Operation op, std::shared_ptr<T> term)
+      : operation_(op), term_(std::move(term)) {}
+
+  Expression::Operation operation_;
+  std::shared_ptr<T> term_;
+};
+
+/// \brief Base class for unbound aggregates.
+class ICEBERG_EXPORT UnboundAggregate : public virtual Expression,
+                                        public Unbound<Expression> {
+ public:
+  ~UnboundAggregate() override = default;
+
+  bool is_unbound_aggregate() const override { return true; }
+};
+
+/// \brief Template for unbound aggregates that carry a term and operation.
+template <typename B>
+class ICEBERG_EXPORT UnboundAggregateImpl : public UnboundAggregate,
+                                            public Aggregate<UnboundTerm<B>> {
+  using BASE = Aggregate<UnboundTerm<B>>;
+
+ public:
+  enum class CountMode { kNonNull, kNull, kStar, kNone };
+
+  UnboundAggregateImpl(Expression::Operation op, std::shared_ptr<UnboundTerm<B>> term,
+                       CountMode count_mode = CountMode::kNone)
+      : BASE(op, std::move(term)), count_mode_(count_mode) {}
+
+  std::shared_ptr<NamedReference> reference() override {
+    return BASE::term() ? BASE::term()->reference() : nullptr;
+  }
+
+  Result<std::shared_ptr<Expression>> Bind(const Schema& schema,
+                                           bool case_sensitive) const override;
+
+  CountMode count_mode() const { return count_mode_; }
+
+ private:
+  CountMode count_mode_;
+};
+
+/// \brief Base class for bound aggregates.
+class ICEBERG_EXPORT BoundAggregate : public Aggregate<BoundTerm>, public Bound {
+ public:
+  using Aggregate<BoundTerm>::op;
+  using Aggregate<BoundTerm>::term;
+
+  std::shared_ptr<BoundReference> reference() override {
+    return term_ ? term_->reference() : nullptr;
+  }
+
+  Result<Literal> Evaluate(const StructLike& data) const override = 0;
+
+  bool is_bound_aggregate() const override { return true; }
+
+  enum class Kind : int8_t {
+    // Count aggregates (COUNT, COUNT_STAR, COUNT_NULL)
+    kCount = 0,
+    // Value aggregates (MIN, MAX)
+    kValue,
+  };
+
+  virtual Kind kind() const = 0;
+
+ protected:
+  BoundAggregate(Expression::Operation op, std::shared_ptr<BoundTerm> term)
+      : Aggregate<BoundTerm>(op, std::move(term)) {}
+};
+
+/// \brief Bound COUNT aggregate.
+class ICEBERG_EXPORT BoundCountAggregate : public BoundAggregate {
+ public:
+  enum class Mode { kNonNull, kNull, kStar };
+
+  BoundCountAggregate(Expression::Operation op, Mode mode,
+                      std::shared_ptr<BoundTerm> term);
+
+  Mode mode() const { return mode_; }
+
+  Kind kind() const override { return Kind::kCount; }
+
+  std::string ToString() const override;
+  Result<Literal> Evaluate(const StructLike& data) const override;

Review Comment:
   ```suggestion
   
     Result<Literal> Evaluate(const StructLike& data) const override;
   ```



##########
src/iceberg/expression/aggregate.h:
##########
@@ -0,0 +1,175 @@
+/*
+ * 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/aggregate.h
+/// Aggregate expression definitions.
+
+#include <concepts>
+#include <memory>
+#include <vector>
+
+#include "iceberg/expression/expression.h"
+#include "iceberg/expression/term.h"
+#include "iceberg/result.h"
+
+namespace iceberg {
+
+template <typename T>
+concept AggregateTermType = std::derived_from<T, Term>;
+
+/// \brief Base aggregate holding an operation and a term.
+template <AggregateTermType T>
+class ICEBERG_EXPORT Aggregate : public virtual Expression {
+ public:
+  ~Aggregate() override = default;
+
+  Expression::Operation op() const override { return operation_; }
+
+  const std::shared_ptr<T>& term() const { return term_; }
+
+ protected:
+  Aggregate(Expression::Operation op, std::shared_ptr<T> term)
+      : operation_(op), term_(std::move(term)) {}
+
+  Expression::Operation operation_;
+  std::shared_ptr<T> term_;
+};
+
+/// \brief Base class for unbound aggregates.
+class ICEBERG_EXPORT UnboundAggregate : public virtual Expression,
+                                        public Unbound<Expression> {
+ public:
+  ~UnboundAggregate() override = default;
+
+  bool is_unbound_aggregate() const override { return true; }
+};
+
+/// \brief Template for unbound aggregates that carry a term and operation.
+template <typename B>
+class ICEBERG_EXPORT UnboundAggregateImpl : public UnboundAggregate,
+                                            public Aggregate<UnboundTerm<B>> {
+  using BASE = Aggregate<UnboundTerm<B>>;
+
+ public:
+  enum class CountMode { kNonNull, kNull, kStar, kNone };
+
+  UnboundAggregateImpl(Expression::Operation op, std::shared_ptr<UnboundTerm<B>> term,
+                       CountMode count_mode = CountMode::kNone)
+      : BASE(op, std::move(term)), count_mode_(count_mode) {}
+
+  std::shared_ptr<NamedReference> reference() override {
+    return BASE::term() ? BASE::term()->reference() : nullptr;
+  }
+
+  Result<std::shared_ptr<Expression>> Bind(const Schema& schema,
+                                           bool case_sensitive) const override;
+
+  CountMode count_mode() const { return count_mode_; }
+
+ private:
+  CountMode count_mode_;
+};
+
+/// \brief Base class for bound aggregates.
+class ICEBERG_EXPORT BoundAggregate : public Aggregate<BoundTerm>, public Bound {
+ public:
+  using Aggregate<BoundTerm>::op;
+  using Aggregate<BoundTerm>::term;
+
+  std::shared_ptr<BoundReference> reference() override {
+    return term_ ? term_->reference() : nullptr;
+  }
+
+  Result<Literal> Evaluate(const StructLike& data) const override = 0;
+
+  bool is_bound_aggregate() const override { return true; }
+
+  enum class Kind : int8_t {
+    // Count aggregates (COUNT, COUNT_STAR, COUNT_NULL)
+    kCount = 0,
+    // Value aggregates (MIN, MAX)
+    kValue,
+  };
+
+  virtual Kind kind() const = 0;
+
+ protected:
+  BoundAggregate(Expression::Operation op, std::shared_ptr<BoundTerm> term)
+      : Aggregate<BoundTerm>(op, std::move(term)) {}
+};
+
+/// \brief Bound COUNT aggregate.
+class ICEBERG_EXPORT BoundCountAggregate : public BoundAggregate {

Review Comment:
   ```suggestion
   class ICEBERG_EXPORT CountAggregate : public BoundAggregate {
   ```



##########
src/iceberg/expression/expression_visitor.h:
##########
@@ -77,6 +78,22 @@ class ICEBERG_EXPORT ExpressionVisitor {
   /// \brief Visit an unbound predicate.
   /// \param pred The unbound predicate to visit
   virtual Result<R> Predicate(const std::shared_ptr<UnboundPredicate>& pred) = 0;
+
+  /// \brief Visit a bound aggregate.
+  /// \param aggregate The bound aggregate to visit.
+  virtual Result<R> Aggregate(const std::shared_ptr<BoundAggregate>& aggregate) {
+    ICEBERG_DCHECK(aggregate != nullptr, "Bound aggregate cannot be null");
+    return NotSupported("Bound aggregate is not supported by this visitor: {}",

Review Comment:
   The error message is misleading since we are printing the aggregate as the visitor name.



##########
src/iceberg/expression/expressions.cc:
##########
@@ -81,6 +82,76 @@ std::shared_ptr<UnboundTransform> Expressions::Transform(
   return unbound_transform;
 }
 
+// Aggregates
+
+std::shared_ptr<UnboundAggregateImpl<BoundReference>> Expressions::Count(
+    std::string name) {
+  return Count(Ref(std::move(name)));
+}
+
+std::shared_ptr<UnboundAggregateImpl<BoundReference>> Expressions::Count(
+    std::shared_ptr<UnboundTerm<BoundReference>> expr) {
+  auto agg = std::make_shared<UnboundAggregateImpl<BoundReference>>(

Review Comment:
   See my other comments. Let's add `Make` functions to these types and throw when there is any error. Same for other functions in this file.



##########
src/iceberg/expression/aggregate.h:
##########
@@ -0,0 +1,175 @@
+/*
+ * 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/aggregate.h
+/// Aggregate expression definitions.
+
+#include <concepts>
+#include <memory>
+#include <vector>
+
+#include "iceberg/expression/expression.h"
+#include "iceberg/expression/term.h"
+#include "iceberg/result.h"
+
+namespace iceberg {
+
+template <typename T>
+concept AggregateTermType = std::derived_from<T, Term>;
+
+/// \brief Base aggregate holding an operation and a term.
+template <AggregateTermType T>
+class ICEBERG_EXPORT Aggregate : public virtual Expression {
+ public:
+  ~Aggregate() override = default;
+
+  Expression::Operation op() const override { return operation_; }
+
+  const std::shared_ptr<T>& term() const { return term_; }
+
+ protected:
+  Aggregate(Expression::Operation op, std::shared_ptr<T> term)
+      : operation_(op), term_(std::move(term)) {}
+
+  Expression::Operation operation_;
+  std::shared_ptr<T> term_;
+};
+
+/// \brief Base class for unbound aggregates.
+class ICEBERG_EXPORT UnboundAggregate : public virtual Expression,
+                                        public Unbound<Expression> {
+ public:
+  ~UnboundAggregate() override = default;
+
+  bool is_unbound_aggregate() const override { return true; }
+};
+
+/// \brief Template for unbound aggregates that carry a term and operation.
+template <typename B>
+class ICEBERG_EXPORT UnboundAggregateImpl : public UnboundAggregate,
+                                            public Aggregate<UnboundTerm<B>> {
+  using BASE = Aggregate<UnboundTerm<B>>;
+
+ public:
+  enum class CountMode { kNonNull, kNull, kStar, kNone };
+
+  UnboundAggregateImpl(Expression::Operation op, std::shared_ptr<UnboundTerm<B>> term,
+                       CountMode count_mode = CountMode::kNone)
+      : BASE(op, std::move(term)), count_mode_(count_mode) {}
+
+  std::shared_ptr<NamedReference> reference() override {
+    return BASE::term() ? BASE::term()->reference() : nullptr;
+  }
+
+  Result<std::shared_ptr<Expression>> Bind(const Schema& schema,
+                                           bool case_sensitive) const override;
+
+  CountMode count_mode() const { return count_mode_; }
+
+ private:
+  CountMode count_mode_;
+};
+
+/// \brief Base class for bound aggregates.
+class ICEBERG_EXPORT BoundAggregate : public Aggregate<BoundTerm>, public Bound {
+ public:
+  using Aggregate<BoundTerm>::op;
+  using Aggregate<BoundTerm>::term;
+
+  std::shared_ptr<BoundReference> reference() override {
+    return term_ ? term_->reference() : nullptr;
+  }
+
+  Result<Literal> Evaluate(const StructLike& data) const override = 0;
+
+  bool is_bound_aggregate() const override { return true; }
+
+  enum class Kind : int8_t {
+    // Count aggregates (COUNT, COUNT_STAR, COUNT_NULL)
+    kCount = 0,
+    // Value aggregates (MIN, MAX)
+    kValue,
+  };
+
+  virtual Kind kind() const = 0;
+
+ protected:
+  BoundAggregate(Expression::Operation op, std::shared_ptr<BoundTerm> term)
+      : Aggregate<BoundTerm>(op, std::move(term)) {}
+};
+
+/// \brief Bound COUNT aggregate.
+class ICEBERG_EXPORT BoundCountAggregate : public BoundAggregate {
+ public:
+  enum class Mode { kNonNull, kNull, kStar };
+
+  BoundCountAggregate(Expression::Operation op, Mode mode,
+                      std::shared_ptr<BoundTerm> term);
+
+  Mode mode() const { return mode_; }
+
+  Kind kind() const override { return Kind::kCount; }
+
+  std::string ToString() const override;
+  Result<Literal> Evaluate(const StructLike& data) const override;
+
+ private:
+  Mode mode_;
+};
+
+/// \brief Bound MAX/MIN aggregate.
+class ICEBERG_EXPORT BoundValueAggregate : public BoundAggregate {
+ public:
+  BoundValueAggregate(Expression::Operation op, std::shared_ptr<BoundTerm> term);
+
+  Kind kind() const override { return Kind::kValue; }
+
+  std::string ToString() const override;
+  Result<Literal> Evaluate(const StructLike& data) const override;
+};
+
+/// \brief Evaluates bound aggregates over StructLike rows.
+class ICEBERG_EXPORT AggregateEvaluator {
+ public:
+  virtual ~AggregateEvaluator() = default;
+
+  /// \brief Create an evaluator for a single bound aggregate.
+  /// \param aggregate The bound aggregate to evaluate across rows.
+  static Result<std::unique_ptr<AggregateEvaluator>> Make(
+      std::shared_ptr<BoundAggregate> aggregate);
+
+  /// \brief Create an evaluator for multiple bound aggregates.
+  /// \param aggregates Aggregates to evaluate in one pass; order is preserved in
+  /// Results().
+  static Result<std::unique_ptr<AggregateEvaluator>> MakeList(
+      std::vector<std::shared_ptr<BoundAggregate>> aggregates);
+
+  /// \brief Add a row to the aggregate.
+  virtual Status Add(const StructLike& row) = 0;

Review Comment:
   Update?



##########
src/iceberg/expression/aggregate.cc:
##########
@@ -0,0 +1,256 @@
+/*
+ * 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/aggregate.h"
+
+#include <format>
+#include <optional>
+#include <vector>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/type.h"
+#include "iceberg/util/checked_cast.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+Result<std::shared_ptr<PrimitiveType>> GetPrimitiveType(const BoundTerm& term) {
+  auto primitive = std::dynamic_pointer_cast<PrimitiveType>(term.type());
+  if (primitive == nullptr) {
+    return InvalidExpression("Aggregate requires primitive type, got {}",
+                             term.type()->ToString());
+  }
+  return primitive;
+}
+
+}  // namespace
+
+// -------------------- Bound aggregates --------------------
+
+BoundCountAggregate::BoundCountAggregate(Expression::Operation op, Mode mode,
+                                         std::shared_ptr<BoundTerm> term)
+    : BoundAggregate(op, std::move(term)), mode_(mode) {}
+
+std::string BoundCountAggregate::ToString() const {
+  if (mode_ == Mode::kStar) {
+    return "count(*)";
+  }
+  ICEBERG_DCHECK(term() != nullptr, "Bound count aggregate should have term");
+  switch (mode_) {
+    case Mode::kNull:
+      return std::format("count_null({})", term()->reference()->name());
+    case Mode::kNonNull:
+      return std::format("count({})", term()->reference()->name());
+    case Mode::kStar:
+      break;
+  }
+  std::unreachable();
+}
+
+Result<Literal> BoundCountAggregate::Evaluate(const StructLike& data) const {

Review Comment:
   Personally I prefer to mimic the Java class design if there is no compelling reason to deviate. 



##########
src/iceberg/expression/aggregate.h:
##########
@@ -0,0 +1,175 @@
+/*
+ * 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/aggregate.h
+/// Aggregate expression definitions.
+
+#include <concepts>
+#include <memory>
+#include <vector>
+
+#include "iceberg/expression/expression.h"
+#include "iceberg/expression/term.h"
+#include "iceberg/result.h"
+
+namespace iceberg {
+
+template <typename T>
+concept AggregateTermType = std::derived_from<T, Term>;
+
+/// \brief Base aggregate holding an operation and a term.
+template <AggregateTermType T>
+class ICEBERG_EXPORT Aggregate : public virtual Expression {
+ public:
+  ~Aggregate() override = default;
+
+  Expression::Operation op() const override { return operation_; }
+
+  const std::shared_ptr<T>& term() const { return term_; }
+
+ protected:
+  Aggregate(Expression::Operation op, std::shared_ptr<T> term)
+      : operation_(op), term_(std::move(term)) {}
+
+  Expression::Operation operation_;
+  std::shared_ptr<T> term_;
+};
+
+/// \brief Base class for unbound aggregates.
+class ICEBERG_EXPORT UnboundAggregate : public virtual Expression,
+                                        public Unbound<Expression> {
+ public:
+  ~UnboundAggregate() override = default;
+
+  bool is_unbound_aggregate() const override { return true; }
+};
+
+/// \brief Template for unbound aggregates that carry a term and operation.
+template <typename B>
+class ICEBERG_EXPORT UnboundAggregateImpl : public UnboundAggregate,
+                                            public Aggregate<UnboundTerm<B>> {
+  using BASE = Aggregate<UnboundTerm<B>>;
+
+ public:
+  enum class CountMode { kNonNull, kNull, kStar, kNone };
+
+  UnboundAggregateImpl(Expression::Operation op, std::shared_ptr<UnboundTerm<B>> term,
+                       CountMode count_mode = CountMode::kNone)
+      : BASE(op, std::move(term)), count_mode_(count_mode) {}
+
+  std::shared_ptr<NamedReference> reference() override {
+    return BASE::term() ? BASE::term()->reference() : nullptr;
+  }
+
+  Result<std::shared_ptr<Expression>> Bind(const Schema& schema,
+                                           bool case_sensitive) const override;
+
+  CountMode count_mode() const { return count_mode_; }
+
+ private:
+  CountMode count_mode_;
+};
+
+/// \brief Base class for bound aggregates.
+class ICEBERG_EXPORT BoundAggregate : public Aggregate<BoundTerm>, public Bound {
+ public:
+  using Aggregate<BoundTerm>::op;
+  using Aggregate<BoundTerm>::term;
+
+  std::shared_ptr<BoundReference> reference() override {
+    return term_ ? term_->reference() : nullptr;

Review Comment:
   ditto for ICEBERG_DCHECK



##########
src/iceberg/expression/aggregate.h:
##########
@@ -0,0 +1,175 @@
+/*
+ * 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/aggregate.h
+/// Aggregate expression definitions.
+
+#include <concepts>
+#include <memory>
+#include <vector>
+
+#include "iceberg/expression/expression.h"
+#include "iceberg/expression/term.h"
+#include "iceberg/result.h"
+
+namespace iceberg {
+
+template <typename T>
+concept AggregateTermType = std::derived_from<T, Term>;
+
+/// \brief Base aggregate holding an operation and a term.
+template <AggregateTermType T>
+class ICEBERG_EXPORT Aggregate : public virtual Expression {
+ public:
+  ~Aggregate() override = default;
+
+  Expression::Operation op() const override { return operation_; }
+
+  const std::shared_ptr<T>& term() const { return term_; }
+
+ protected:
+  Aggregate(Expression::Operation op, std::shared_ptr<T> term)
+      : operation_(op), term_(std::move(term)) {}
+
+  Expression::Operation operation_;
+  std::shared_ptr<T> term_;
+};
+
+/// \brief Base class for unbound aggregates.
+class ICEBERG_EXPORT UnboundAggregate : public virtual Expression,
+                                        public Unbound<Expression> {
+ public:
+  ~UnboundAggregate() override = default;
+
+  bool is_unbound_aggregate() const override { return true; }
+};
+
+/// \brief Template for unbound aggregates that carry a term and operation.
+template <typename B>
+class ICEBERG_EXPORT UnboundAggregateImpl : public UnboundAggregate,
+                                            public Aggregate<UnboundTerm<B>> {
+  using BASE = Aggregate<UnboundTerm<B>>;
+
+ public:
+  enum class CountMode { kNonNull, kNull, kStar, kNone };
+
+  UnboundAggregateImpl(Expression::Operation op, std::shared_ptr<UnboundTerm<B>> term,
+                       CountMode count_mode = CountMode::kNone)
+      : BASE(op, std::move(term)), count_mode_(count_mode) {}
+
+  std::shared_ptr<NamedReference> reference() override {
+    return BASE::term() ? BASE::term()->reference() : nullptr;

Review Comment:
   nit: add a `ICEBERG_DCHECK` for expected `op`



##########
src/iceberg/expression/aggregate.h:
##########
@@ -0,0 +1,175 @@
+/*
+ * 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/aggregate.h
+/// Aggregate expression definitions.
+
+#include <concepts>
+#include <memory>
+#include <vector>
+
+#include "iceberg/expression/expression.h"
+#include "iceberg/expression/term.h"
+#include "iceberg/result.h"
+
+namespace iceberg {
+
+template <typename T>
+concept AggregateTermType = std::derived_from<T, Term>;
+
+/// \brief Base aggregate holding an operation and a term.
+template <AggregateTermType T>
+class ICEBERG_EXPORT Aggregate : public virtual Expression {
+ public:
+  ~Aggregate() override = default;
+
+  Expression::Operation op() const override { return operation_; }
+
+  const std::shared_ptr<T>& term() const { return term_; }
+
+ protected:
+  Aggregate(Expression::Operation op, std::shared_ptr<T> term)
+      : operation_(op), term_(std::move(term)) {}
+
+  Expression::Operation operation_;
+  std::shared_ptr<T> term_;
+};
+
+/// \brief Base class for unbound aggregates.
+class ICEBERG_EXPORT UnboundAggregate : public virtual Expression,
+                                        public Unbound<Expression> {
+ public:
+  ~UnboundAggregate() override = default;
+
+  bool is_unbound_aggregate() const override { return true; }
+};
+
+/// \brief Template for unbound aggregates that carry a term and operation.
+template <typename B>
+class ICEBERG_EXPORT UnboundAggregateImpl : public UnboundAggregate,
+                                            public Aggregate<UnboundTerm<B>> {
+  using BASE = Aggregate<UnboundTerm<B>>;
+
+ public:
+  enum class CountMode { kNonNull, kNull, kStar, kNone };
+
+  UnboundAggregateImpl(Expression::Operation op, std::shared_ptr<UnboundTerm<B>> term,
+                       CountMode count_mode = CountMode::kNone)
+      : BASE(op, std::move(term)), count_mode_(count_mode) {}

Review Comment:
   Please add a static `Make` function and use private constructor so we can make sure that created expression does not have invalid fields.



##########
src/iceberg/expression/expressions.h:
##########
@@ -101,6 +102,48 @@ class ICEBERG_EXPORT Expressions {
   static std::shared_ptr<UnboundTransform> Transform(
       std::string name, std::shared_ptr<Transform> transform);
 
+  // Aggregates
+
+  /// \brief Create COUNT(col) aggregate.

Review Comment:
   ```suggestion
     /// \brief Create a COUNT aggregate for a field name.
   ```
   
   Let's be consistent with the above comment style. Same for all below.



##########
src/iceberg/expression/aggregate.h:
##########
@@ -0,0 +1,175 @@
+/*
+ * 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/aggregate.h
+/// Aggregate expression definitions.
+
+#include <concepts>
+#include <memory>
+#include <vector>
+
+#include "iceberg/expression/expression.h"
+#include "iceberg/expression/term.h"
+#include "iceberg/result.h"
+
+namespace iceberg {
+
+template <typename T>
+concept AggregateTermType = std::derived_from<T, Term>;
+
+/// \brief Base aggregate holding an operation and a term.
+template <AggregateTermType T>
+class ICEBERG_EXPORT Aggregate : public virtual Expression {
+ public:
+  ~Aggregate() override = default;
+
+  Expression::Operation op() const override { return operation_; }
+
+  const std::shared_ptr<T>& term() const { return term_; }
+
+ protected:
+  Aggregate(Expression::Operation op, std::shared_ptr<T> term)
+      : operation_(op), term_(std::move(term)) {}
+
+  Expression::Operation operation_;
+  std::shared_ptr<T> term_;
+};
+
+/// \brief Base class for unbound aggregates.
+class ICEBERG_EXPORT UnboundAggregate : public virtual Expression,
+                                        public Unbound<Expression> {
+ public:
+  ~UnboundAggregate() override = default;
+
+  bool is_unbound_aggregate() const override { return true; }
+};
+
+/// \brief Template for unbound aggregates that carry a term and operation.
+template <typename B>
+class ICEBERG_EXPORT UnboundAggregateImpl : public UnboundAggregate,
+                                            public Aggregate<UnboundTerm<B>> {
+  using BASE = Aggregate<UnboundTerm<B>>;
+
+ public:
+  enum class CountMode { kNonNull, kNull, kStar, kNone };
+
+  UnboundAggregateImpl(Expression::Operation op, std::shared_ptr<UnboundTerm<B>> term,
+                       CountMode count_mode = CountMode::kNone)
+      : BASE(op, std::move(term)), count_mode_(count_mode) {}
+
+  std::shared_ptr<NamedReference> reference() override {
+    return BASE::term() ? BASE::term()->reference() : nullptr;
+  }
+
+  Result<std::shared_ptr<Expression>> Bind(const Schema& schema,
+                                           bool case_sensitive) const override;
+
+  CountMode count_mode() const { return count_mode_; }
+
+ private:
+  CountMode count_mode_;
+};
+
+/// \brief Base class for bound aggregates.
+class ICEBERG_EXPORT BoundAggregate : public Aggregate<BoundTerm>, public Bound {
+ public:
+  using Aggregate<BoundTerm>::op;
+  using Aggregate<BoundTerm>::term;
+
+  std::shared_ptr<BoundReference> reference() override {
+    return term_ ? term_->reference() : nullptr;
+  }
+
+  Result<Literal> Evaluate(const StructLike& data) const override = 0;
+
+  bool is_bound_aggregate() const override { return true; }
+
+  enum class Kind : int8_t {
+    // Count aggregates (COUNT, COUNT_STAR, COUNT_NULL)
+    kCount = 0,
+    // Value aggregates (MIN, MAX)
+    kValue,
+  };
+
+  virtual Kind kind() const = 0;
+
+ protected:
+  BoundAggregate(Expression::Operation op, std::shared_ptr<BoundTerm> term)
+      : Aggregate<BoundTerm>(op, std::move(term)) {}
+};
+
+/// \brief Bound COUNT aggregate.
+class ICEBERG_EXPORT BoundCountAggregate : public BoundAggregate {
+ public:
+  enum class Mode { kNonNull, kNull, kStar };
+
+  BoundCountAggregate(Expression::Operation op, Mode mode,
+                      std::shared_ptr<BoundTerm> term);
+
+  Mode mode() const { return mode_; }
+
+  Kind kind() const override { return Kind::kCount; }
+
+  std::string ToString() const override;
+  Result<Literal> Evaluate(const StructLike& data) const override;
+
+ private:
+  Mode mode_;
+};
+
+/// \brief Bound MAX/MIN aggregate.
+class ICEBERG_EXPORT BoundValueAggregate : public BoundAggregate {
+ public:
+  BoundValueAggregate(Expression::Operation op, std::shared_ptr<BoundTerm> term);

Review Comment:
   ditto



##########
src/iceberg/expression/aggregate.h:
##########
@@ -0,0 +1,175 @@
+/*
+ * 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/aggregate.h
+/// Aggregate expression definitions.
+
+#include <concepts>
+#include <memory>
+#include <vector>
+
+#include "iceberg/expression/expression.h"
+#include "iceberg/expression/term.h"
+#include "iceberg/result.h"
+
+namespace iceberg {
+
+template <typename T>
+concept AggregateTermType = std::derived_from<T, Term>;
+
+/// \brief Base aggregate holding an operation and a term.
+template <AggregateTermType T>
+class ICEBERG_EXPORT Aggregate : public virtual Expression {
+ public:
+  ~Aggregate() override = default;
+
+  Expression::Operation op() const override { return operation_; }
+
+  const std::shared_ptr<T>& term() const { return term_; }
+
+ protected:
+  Aggregate(Expression::Operation op, std::shared_ptr<T> term)
+      : operation_(op), term_(std::move(term)) {}
+
+  Expression::Operation operation_;
+  std::shared_ptr<T> term_;
+};
+
+/// \brief Base class for unbound aggregates.
+class ICEBERG_EXPORT UnboundAggregate : public virtual Expression,
+                                        public Unbound<Expression> {
+ public:
+  ~UnboundAggregate() override = default;
+
+  bool is_unbound_aggregate() const override { return true; }
+};
+
+/// \brief Template for unbound aggregates that carry a term and operation.
+template <typename B>
+class ICEBERG_EXPORT UnboundAggregateImpl : public UnboundAggregate,
+                                            public Aggregate<UnboundTerm<B>> {
+  using BASE = Aggregate<UnboundTerm<B>>;
+
+ public:
+  enum class CountMode { kNonNull, kNull, kStar, kNone };

Review Comment:
   What is `kNone` for? We don't even need `CountMode`, do we? `UnboundAggregateImpl` should also support value mode.



##########
src/iceberg/expression/aggregate.h:
##########
@@ -0,0 +1,175 @@
+/*
+ * 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/aggregate.h
+/// Aggregate expression definitions.
+
+#include <concepts>
+#include <memory>
+#include <vector>
+
+#include "iceberg/expression/expression.h"
+#include "iceberg/expression/term.h"
+#include "iceberg/result.h"
+
+namespace iceberg {
+
+template <typename T>
+concept AggregateTermType = std::derived_from<T, Term>;

Review Comment:
   nit: `predicate.h` has defined a duplicate `concept TermType`. Perhaps we can move it to `term.h` and reuse it here?



##########
src/iceberg/expression/aggregate.cc:
##########
@@ -0,0 +1,256 @@
+/*
+ * 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/aggregate.h"
+
+#include <format>
+#include <optional>
+#include <vector>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/type.h"
+#include "iceberg/util/checked_cast.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+Result<std::shared_ptr<PrimitiveType>> GetPrimitiveType(const BoundTerm& term) {
+  auto primitive = std::dynamic_pointer_cast<PrimitiveType>(term.type());
+  if (primitive == nullptr) {
+    return InvalidExpression("Aggregate requires primitive type, got {}",
+                             term.type()->ToString());
+  }
+  return primitive;

Review Comment:
   ```suggestion
     if (!term.type().is_primitive()) {
       return InvalidExpression("Aggregate requires primitive type, got {}",
                                term.type()->ToString());
     }
     return internal::checked_pointer_cast<PrimitiveType>(term.type());
   ```



##########
src/iceberg/expression/aggregate.cc:
##########
@@ -0,0 +1,256 @@
+/*
+ * 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/aggregate.h"
+
+#include <format>
+#include <optional>
+#include <vector>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/type.h"
+#include "iceberg/util/checked_cast.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+Result<std::shared_ptr<PrimitiveType>> GetPrimitiveType(const BoundTerm& term) {
+  auto primitive = std::dynamic_pointer_cast<PrimitiveType>(term.type());
+  if (primitive == nullptr) {
+    return InvalidExpression("Aggregate requires primitive type, got {}",
+                             term.type()->ToString());
+  }
+  return primitive;
+}
+
+}  // namespace
+
+// -------------------- Bound aggregates --------------------
+
+BoundCountAggregate::BoundCountAggregate(Expression::Operation op, Mode mode,
+                                         std::shared_ptr<BoundTerm> term)
+    : BoundAggregate(op, std::move(term)), mode_(mode) {}
+
+std::string BoundCountAggregate::ToString() const {
+  if (mode_ == Mode::kStar) {
+    return "count(*)";
+  }
+  ICEBERG_DCHECK(term() != nullptr, "Bound count aggregate should have term");
+  switch (mode_) {
+    case Mode::kNull:
+      return std::format("count_null({})", term()->reference()->name());
+    case Mode::kNonNull:
+      return std::format("count({})", term()->reference()->name());
+    case Mode::kStar:
+      break;
+  }
+  std::unreachable();
+}
+
+Result<Literal> BoundCountAggregate::Evaluate(const StructLike& data) const {

Review Comment:
   What's the tradeoff between these two options:
   - Current approach that uses a single `CountAggregate` for all kinds.
   - Add dedicated subclass for CountNull, CountNotNull and CountStar.



##########
src/iceberg/expression/aggregate.h:
##########
@@ -0,0 +1,175 @@
+/*
+ * 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/aggregate.h
+/// Aggregate expression definitions.
+
+#include <concepts>
+#include <memory>
+#include <vector>
+
+#include "iceberg/expression/expression.h"
+#include "iceberg/expression/term.h"
+#include "iceberg/result.h"
+
+namespace iceberg {
+
+template <typename T>
+concept AggregateTermType = std::derived_from<T, Term>;
+
+/// \brief Base aggregate holding an operation and a term.
+template <AggregateTermType T>
+class ICEBERG_EXPORT Aggregate : public virtual Expression {
+ public:
+  ~Aggregate() override = default;
+
+  Expression::Operation op() const override { return operation_; }
+
+  const std::shared_ptr<T>& term() const { return term_; }
+
+ protected:
+  Aggregate(Expression::Operation op, std::shared_ptr<T> term)
+      : operation_(op), term_(std::move(term)) {}
+
+  Expression::Operation operation_;
+  std::shared_ptr<T> term_;
+};
+
+/// \brief Base class for unbound aggregates.
+class ICEBERG_EXPORT UnboundAggregate : public virtual Expression,
+                                        public Unbound<Expression> {
+ public:
+  ~UnboundAggregate() override = default;
+
+  bool is_unbound_aggregate() const override { return true; }
+};
+
+/// \brief Template for unbound aggregates that carry a term and operation.
+template <typename B>
+class ICEBERG_EXPORT UnboundAggregateImpl : public UnboundAggregate,
+                                            public Aggregate<UnboundTerm<B>> {
+  using BASE = Aggregate<UnboundTerm<B>>;
+
+ public:
+  enum class CountMode { kNonNull, kNull, kStar, kNone };
+
+  UnboundAggregateImpl(Expression::Operation op, std::shared_ptr<UnboundTerm<B>> term,
+                       CountMode count_mode = CountMode::kNone)
+      : BASE(op, std::move(term)), count_mode_(count_mode) {}
+
+  std::shared_ptr<NamedReference> reference() override {
+    return BASE::term() ? BASE::term()->reference() : nullptr;
+  }
+
+  Result<std::shared_ptr<Expression>> Bind(const Schema& schema,
+                                           bool case_sensitive) const override;
+
+  CountMode count_mode() const { return count_mode_; }
+
+ private:
+  CountMode count_mode_;
+};
+
+/// \brief Base class for bound aggregates.
+class ICEBERG_EXPORT BoundAggregate : public Aggregate<BoundTerm>, public Bound {
+ public:
+  using Aggregate<BoundTerm>::op;
+  using Aggregate<BoundTerm>::term;
+
+  std::shared_ptr<BoundReference> reference() override {
+    return term_ ? term_->reference() : nullptr;
+  }
+
+  Result<Literal> Evaluate(const StructLike& data) const override = 0;
+
+  bool is_bound_aggregate() const override { return true; }
+
+  enum class Kind : int8_t {
+    // Count aggregates (COUNT, COUNT_STAR, COUNT_NULL)
+    kCount = 0,
+    // Value aggregates (MIN, MAX)
+    kValue,
+  };
+
+  virtual Kind kind() const = 0;
+
+ protected:
+  BoundAggregate(Expression::Operation op, std::shared_ptr<BoundTerm> term)
+      : Aggregate<BoundTerm>(op, std::move(term)) {}
+};
+
+/// \brief Bound COUNT aggregate.
+class ICEBERG_EXPORT BoundCountAggregate : public BoundAggregate {
+ public:
+  enum class Mode { kNonNull, kNull, kStar };
+
+  BoundCountAggregate(Expression::Operation op, Mode mode,
+                      std::shared_ptr<BoundTerm> term);
+
+  Mode mode() const { return mode_; }
+
+  Kind kind() const override { return Kind::kCount; }
+
+  std::string ToString() const override;
+  Result<Literal> Evaluate(const StructLike& data) const override;
+
+ private:
+  Mode mode_;
+};
+
+/// \brief Bound MAX/MIN aggregate.
+class ICEBERG_EXPORT BoundValueAggregate : public BoundAggregate {

Review Comment:
   ```suggestion
   class ICEBERG_EXPORT ValueAggregate : public BoundAggregate {
   ```



##########
src/iceberg/expression/aggregate.cc:
##########
@@ -0,0 +1,256 @@
+/*
+ * 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/aggregate.h"
+
+#include <format>
+#include <optional>
+#include <vector>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/type.h"
+#include "iceberg/util/checked_cast.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+Result<std::shared_ptr<PrimitiveType>> GetPrimitiveType(const BoundTerm& term) {
+  auto primitive = std::dynamic_pointer_cast<PrimitiveType>(term.type());
+  if (primitive == nullptr) {
+    return InvalidExpression("Aggregate requires primitive type, got {}",
+                             term.type()->ToString());
+  }
+  return primitive;
+}
+
+}  // namespace
+
+// -------------------- Bound aggregates --------------------
+
+BoundCountAggregate::BoundCountAggregate(Expression::Operation op, Mode mode,
+                                         std::shared_ptr<BoundTerm> term)
+    : BoundAggregate(op, std::move(term)), mode_(mode) {}
+
+std::string BoundCountAggregate::ToString() const {
+  if (mode_ == Mode::kStar) {
+    return "count(*)";
+  }
+  ICEBERG_DCHECK(term() != nullptr, "Bound count aggregate should have term");
+  switch (mode_) {
+    case Mode::kNull:
+      return std::format("count_null({})", term()->reference()->name());
+    case Mode::kNonNull:
+      return std::format("count({})", term()->reference()->name());
+    case Mode::kStar:
+      break;
+  }
+  std::unreachable();
+}
+
+Result<Literal> BoundCountAggregate::Evaluate(const StructLike& data) const {
+  switch (mode_) {
+    case Mode::kStar:
+      return Literal::Long(1);
+    case Mode::kNonNull: {
+      ICEBERG_ASSIGN_OR_RAISE(auto literal, term()->Evaluate(data));
+      return Literal::Long(literal.IsNull() ? 0 : 1);
+    }
+    case Mode::kNull: {
+      ICEBERG_ASSIGN_OR_RAISE(auto literal, term()->Evaluate(data));
+      return Literal::Long(literal.IsNull() ? 1 : 0);
+    }
+  }
+  std::unreachable();
+}
+
+BoundValueAggregate::BoundValueAggregate(Expression::Operation op,
+                                         std::shared_ptr<BoundTerm> term)
+    : BoundAggregate(op, std::move(term)) {}
+
+std::string BoundValueAggregate::ToString() const {
+  ICEBERG_DCHECK(term() != nullptr, "Bound value aggregate should have term");
+  auto prefix = op() == Expression::Operation::kMax ? "max" : "min";
+  return std::format("{}({})", prefix, term()->reference()->name());
+}
+
+Result<Literal> BoundValueAggregate::Evaluate(const StructLike& data) const {
+  ICEBERG_ASSIGN_OR_RAISE(auto literal, term()->Evaluate(data));
+  return literal;
+}
+
+// -------------------- Unbound binding --------------------
+
+template <typename B>
+Result<std::shared_ptr<Expression>> UnboundAggregateImpl<B>::Bind(
+    const Schema& schema, bool case_sensitive) const {
+  std::shared_ptr<B> bound_term;
+  if (this->term()) {
+    ICEBERG_ASSIGN_OR_THROW(bound_term, this->term()->Bind(schema, case_sensitive));
+  }
+
+  switch (count_mode_) {
+    case CountMode::kStar:
+    case CountMode::kNull:
+    case CountMode::kNonNull: {
+      auto op = this->op() == Expression::Operation::kCountStar
+                    ? Expression::Operation::kCountStar
+                    : Expression::Operation::kCount;
+      auto mode =
+          count_mode_ == CountMode::kNull
+              ? BoundCountAggregate::Mode::kNull
+              : (count_mode_ == CountMode::kStar ? BoundCountAggregate::Mode::kStar
+                                                 : BoundCountAggregate::Mode::kNonNull);
+      auto aggregate =
+          std::make_shared<BoundCountAggregate>(op, mode, std::move(bound_term));
+      return aggregate;
+    }
+    case CountMode::kNone: {
+      if (this->op() != Expression::Operation::kMax &&
+          this->op() != Expression::Operation::kMin) {
+        return NotSupported("Unsupported aggregate operation");
+      }
+      if (!bound_term) {
+        return InvalidExpression("Aggregate requires a term");
+      }
+      auto aggregate =
+          std::make_shared<BoundValueAggregate>(this->op(), std::move(bound_term));
+      return aggregate;
+    }
+  }
+  std::unreachable();
+}
+
+template class UnboundAggregateImpl<BoundReference>;
+
+// -------------------- AggregateEvaluator --------------------
+
+namespace {
+
+class AggregateEvaluatorImpl : public AggregateEvaluator {

Review Comment:
   Why not following the Java impl of aggregator class hierarchy? If we need to add support for `DataFile`, these lines will be doubled with a lot of duplicate code.



##########
src/iceberg/expression/aggregate.h:
##########
@@ -0,0 +1,175 @@
+/*
+ * 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/aggregate.h
+/// Aggregate expression definitions.
+
+#include <concepts>
+#include <memory>
+#include <vector>
+
+#include "iceberg/expression/expression.h"
+#include "iceberg/expression/term.h"
+#include "iceberg/result.h"
+
+namespace iceberg {
+
+template <typename T>
+concept AggregateTermType = std::derived_from<T, Term>;
+
+/// \brief Base aggregate holding an operation and a term.
+template <AggregateTermType T>
+class ICEBERG_EXPORT Aggregate : public virtual Expression {
+ public:
+  ~Aggregate() override = default;
+
+  Expression::Operation op() const override { return operation_; }
+
+  const std::shared_ptr<T>& term() const { return term_; }
+
+ protected:
+  Aggregate(Expression::Operation op, std::shared_ptr<T> term)
+      : operation_(op), term_(std::move(term)) {}
+
+  Expression::Operation operation_;
+  std::shared_ptr<T> term_;
+};
+
+/// \brief Base class for unbound aggregates.
+class ICEBERG_EXPORT UnboundAggregate : public virtual Expression,
+                                        public Unbound<Expression> {
+ public:
+  ~UnboundAggregate() override = default;
+
+  bool is_unbound_aggregate() const override { return true; }
+};
+
+/// \brief Template for unbound aggregates that carry a term and operation.
+template <typename B>
+class ICEBERG_EXPORT UnboundAggregateImpl : public UnboundAggregate,
+                                            public Aggregate<UnboundTerm<B>> {
+  using BASE = Aggregate<UnboundTerm<B>>;
+
+ public:
+  enum class CountMode { kNonNull, kNull, kStar, kNone };
+
+  UnboundAggregateImpl(Expression::Operation op, std::shared_ptr<UnboundTerm<B>> term,
+                       CountMode count_mode = CountMode::kNone)
+      : BASE(op, std::move(term)), count_mode_(count_mode) {}
+
+  std::shared_ptr<NamedReference> reference() override {
+    return BASE::term() ? BASE::term()->reference() : nullptr;
+  }
+
+  Result<std::shared_ptr<Expression>> Bind(const Schema& schema,
+                                           bool case_sensitive) const override;
+
+  CountMode count_mode() const { return count_mode_; }
+
+ private:
+  CountMode count_mode_;
+};
+
+/// \brief Base class for bound aggregates.
+class ICEBERG_EXPORT BoundAggregate : public Aggregate<BoundTerm>, public Bound {
+ public:
+  using Aggregate<BoundTerm>::op;
+  using Aggregate<BoundTerm>::term;
+
+  std::shared_ptr<BoundReference> reference() override {
+    return term_ ? term_->reference() : nullptr;
+  }
+
+  Result<Literal> Evaluate(const StructLike& data) const override = 0;
+
+  bool is_bound_aggregate() const override { return true; }
+
+  enum class Kind : int8_t {
+    // Count aggregates (COUNT, COUNT_STAR, COUNT_NULL)
+    kCount = 0,
+    // Value aggregates (MIN, MAX)
+    kValue,
+  };
+
+  virtual Kind kind() const = 0;
+
+ protected:
+  BoundAggregate(Expression::Operation op, std::shared_ptr<BoundTerm> term)
+      : Aggregate<BoundTerm>(op, std::move(term)) {}
+};
+
+/// \brief Bound COUNT aggregate.
+class ICEBERG_EXPORT BoundCountAggregate : public BoundAggregate {
+ public:
+  enum class Mode { kNonNull, kNull, kStar };
+
+  BoundCountAggregate(Expression::Operation op, Mode mode,

Review Comment:
   Same here. Make the ctor private and add a static `Make` function.



-- 
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: issues-unsubscribe@iceberg.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org


Re: [PR] feat: add aggregate expressions and evaluator [iceberg-cpp]

Posted by "SuKi2cn (via GitHub)" <gi...@apache.org>.
SuKi2cn commented on PR #335:
URL: https://github.com/apache/iceberg-cpp/pull/335#issuecomment-3568929864

   @wgtmac @Jinchul81 Thanks for the review, I'll take some time to address these changes and follow up shortly.
   


-- 
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: issues-unsubscribe@iceberg.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org


Re: [PR] feat: add aggregate expressions and evaluator [iceberg-cpp]

Posted by "wgtmac (via GitHub)" <gi...@apache.org>.
wgtmac merged PR #335:
URL: https://github.com/apache/iceberg-cpp/pull/335


-- 
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: issues-unsubscribe@iceberg.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org


Re: [PR] feat: add aggregate expressions and evaluator [iceberg-cpp]

Posted by "wgtmac (via GitHub)" <gi...@apache.org>.
wgtmac commented on code in PR #335:
URL: https://github.com/apache/iceberg-cpp/pull/335#discussion_r2562549187


##########
src/iceberg/test/aggregate_test.cc:
##########
@@ -0,0 +1,160 @@
+/*
+ * 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/aggregate.h"
+
+#include <gtest/gtest.h>
+
+#include "iceberg/exception.h"
+#include "iceberg/expression/binder.h"
+#include "iceberg/expression/expressions.h"
+#include "iceberg/row/struct_like.h"
+#include "iceberg/schema.h"
+#include "iceberg/test/matchers.h"
+#include "iceberg/type.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {

Review Comment:
   These test cases look insufficient to me. I think we need to covert at least the following:
   
   - Verify unbound aggregates of different operations are created successfully and invalid inputs are rejected.
   - Verify binding unbound to bound aggregates work as expected for different operations.
   - Verify ToString and other member functions work.
   - Verify bound aggregates evaluation work as expected.
   - Verify different evaluators work fine for all operations and single/multiple aggregate inputs.



-- 
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: issues-unsubscribe@iceberg.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org