| /* | |
| * 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/expression/binder.h" | |
| #include "iceberg/expression/expressions.h" | |
| #include "iceberg/manifest/manifest_entry.h" | |
| #include "iceberg/row/struct_like.h" | |
| #include "iceberg/schema.h" | |
| #include "iceberg/test/matchers.h" | |
| #include "iceberg/type.h" | |
| namespace iceberg { | |
| namespace { | |
| /// XXX: `Scalar` carries view semantics, so it is unsafe to use std::string_view variant. | |
| 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); | |
| } | |
| return fields_[pos]; | |
| } | |
| size_t num_fields() const override { return fields_.size(); } | |
| private: | |
| std::vector<Scalar> fields_; | |
| }; | |
| std::shared_ptr<BoundAggregate> BindAggregate(const Schema& schema, | |
| const std::shared_ptr<Expression>& expr) { | |
| auto result = Binder::Bind(schema, expr, /*case_sensitive=*/true); | |
| EXPECT_TRUE(result.has_value()) | |
| << "Failed to bind aggregate: " << result.error().message; | |
| auto bound = std::dynamic_pointer_cast<BoundAggregate>(std::move(result).value()); | |
| EXPECT_NE(bound, nullptr); | |
| return bound; | |
| } | |
| } // namespace | |
| TEST(AggregateTest, CountVariants) { | |
| Schema schema({SchemaField::MakeOptional(1, "id", int32()), | |
| SchemaField::MakeOptional(2, "value", int32())}); | |
| auto count_expr = Expressions::Count("id"); | |
| auto count_bound = BindAggregate(schema, count_expr); | |
| auto count_evaluator = AggregateEvaluator::Make(count_bound).value(); | |
| auto count_null_expr = Expressions::CountNull("value"); | |
| auto count_null_bound = BindAggregate(schema, count_null_expr); | |
| auto count_null_evaluator = AggregateEvaluator::Make(count_null_bound).value(); | |
| auto count_star_expr = Expressions::CountStar(); | |
| auto count_star_bound = BindAggregate(schema, count_star_expr); | |
| auto count_star_evaluator = AggregateEvaluator::Make(count_star_bound).value(); | |
| std::vector<VectorStructLike> rows{ | |
| VectorStructLike({Scalar{int32_t{1}}, Scalar{int32_t{10}}}), | |
| VectorStructLike({Scalar{int32_t{2}}, Scalar{std::monostate{}}}), | |
| VectorStructLike({Scalar{std::monostate{}}, Scalar{int32_t{30}}})}; | |
| for (const auto& row : rows) { | |
| ASSERT_TRUE(count_evaluator->Update(row).has_value()); | |
| ASSERT_TRUE(count_null_evaluator->Update(row).has_value()); | |
| ASSERT_TRUE(count_star_evaluator->Update(row).has_value()); | |
| } | |
| ICEBERG_UNWRAP_OR_FAIL(auto count_result, count_evaluator->GetResult()); | |
| EXPECT_EQ(std::get<int64_t>(count_result.value()), 2); | |
| ICEBERG_UNWRAP_OR_FAIL(auto count_null_result, count_null_evaluator->GetResult()); | |
| EXPECT_EQ(std::get<int64_t>(count_null_result.value()), 1); | |
| ICEBERG_UNWRAP_OR_FAIL(auto count_star_result, count_star_evaluator->GetResult()); | |
| EXPECT_EQ(std::get<int64_t>(count_star_result.value()), 3); | |
| } | |
| TEST(AggregateTest, MaxMinAggregates) { | |
| Schema schema({SchemaField::MakeOptional(1, "value", int32())}); | |
| auto max_expr = Expressions::Max("value"); | |
| auto min_expr = Expressions::Min("value"); | |
| auto max_bound = BindAggregate(schema, max_expr); | |
| auto min_bound = BindAggregate(schema, min_expr); | |
| auto max_eval = AggregateEvaluator::Make(max_bound).value(); | |
| auto min_eval = AggregateEvaluator::Make(min_bound).value(); | |
| std::vector<VectorStructLike> rows{VectorStructLike({Scalar{int32_t{5}}}), | |
| VectorStructLike({Scalar{std::monostate{}}}), | |
| VectorStructLike({Scalar{int32_t{2}}}), | |
| VectorStructLike({Scalar{int32_t{12}}})}; | |
| for (const auto& row : rows) { | |
| ASSERT_TRUE(max_eval->Update(row).has_value()); | |
| ASSERT_TRUE(min_eval->Update(row).has_value()); | |
| } | |
| ICEBERG_UNWRAP_OR_FAIL(auto max_result, max_eval->GetResult()); | |
| EXPECT_EQ(std::get<int32_t>(max_result.value()), 12); | |
| ICEBERG_UNWRAP_OR_FAIL(auto min_result, min_eval->GetResult()); | |
| EXPECT_EQ(std::get<int32_t>(min_result.value()), 2); | |
| } | |
| TEST(AggregateTest, UnboundAggregateCreationAndBinding) { | |
| Schema schema({SchemaField::MakeOptional(1, "id", int32()), | |
| SchemaField::MakeOptional(2, "value", int32())}); | |
| auto count = Expressions::Count("id"); | |
| auto count_null = Expressions::CountNull("id"); | |
| auto count_star = Expressions::CountStar(); | |
| auto max = Expressions::Max("value"); | |
| auto min = Expressions::Min("value"); | |
| EXPECT_EQ(count->ToString(), "count(ref(name=\"id\"))"); | |
| EXPECT_EQ(count_null->ToString(), "count_if(ref(name=\"id\") is null)"); | |
| EXPECT_EQ(count_star->ToString(), "count(*)"); | |
| EXPECT_EQ(max->ToString(), "max(ref(name=\"value\"))"); | |
| EXPECT_EQ(min->ToString(), "min(ref(name=\"value\"))"); | |
| // Bind succeeds for existing columns | |
| EXPECT_TRUE(Binder::Bind(schema, count, /*case_sensitive=*/true).has_value()); | |
| EXPECT_TRUE(Binder::Bind(schema, count_null, /*case_sensitive=*/true).has_value()); | |
| EXPECT_TRUE(Binder::Bind(schema, count_star, /*case_sensitive=*/true).has_value()); | |
| EXPECT_TRUE(Binder::Bind(schema, max, /*case_sensitive=*/true).has_value()); | |
| EXPECT_TRUE(Binder::Bind(schema, min, /*case_sensitive=*/true).has_value()); | |
| // Binding fails when the reference is missing | |
| auto missing_count = Expressions::Count("missing"); | |
| auto missing_bind = Binder::Bind(schema, missing_count, /*case_sensitive=*/true); | |
| EXPECT_THAT(missing_bind, IsError(ErrorKind::kInvalidExpression)); | |
| // Creating a value aggregate with null term should fail | |
| auto invalid_unbound = | |
| UnboundAggregateImpl<BoundReference>::Make(Expression::Operation::kMax, nullptr); | |
| EXPECT_THAT(invalid_unbound, IsError(ErrorKind::kInvalidExpression)); | |
| } | |
| TEST(AggregateTest, BoundAggregateEvaluateDirectly) { | |
| Schema schema({SchemaField::MakeOptional(1, "id", int32()), | |
| SchemaField::MakeOptional(2, "value", int32())}); | |
| auto count_bound = BindAggregate(schema, Expressions::Count("id")); | |
| auto count_null_bound = BindAggregate(schema, Expressions::CountNull("value")); | |
| auto max_bound = BindAggregate(schema, Expressions::Max("value")); | |
| std::vector<VectorStructLike> rows{ | |
| VectorStructLike({Scalar{int32_t{1}}, Scalar{int32_t{10}}}), | |
| VectorStructLike({Scalar{int32_t{2}}, Scalar{std::monostate{}}}), | |
| VectorStructLike({Scalar{std::monostate{}}, Scalar{int32_t{30}}}), | |
| VectorStructLike({Scalar{int32_t{3}}, Scalar{int32_t{2}}})}; | |
| int64_t count_sum = 0; | |
| int64_t count_null_sum = 0; | |
| int32_t max_val = std::numeric_limits<int32_t>::min(); | |
| for (const auto& row : rows) { | |
| ICEBERG_UNWRAP_OR_FAIL(auto c, count_bound->Evaluate(row)); | |
| count_sum += std::get<int64_t>(c.value()); | |
| ICEBERG_UNWRAP_OR_FAIL(auto cn, count_null_bound->Evaluate(row)); | |
| count_null_sum += std::get<int64_t>(cn.value()); | |
| ICEBERG_UNWRAP_OR_FAIL(auto mv, max_bound->Evaluate(row)); | |
| if (!mv.IsNull()) { | |
| max_val = std::max(max_val, std::get<int32_t>(mv.value())); | |
| } | |
| } | |
| EXPECT_EQ(count_sum, 3); | |
| EXPECT_EQ(count_null_sum, 1); | |
| EXPECT_EQ(max_val, 30); | |
| } | |
| TEST(AggregateTest, MultipleAggregatesInEvaluator) { | |
| Schema schema({SchemaField::MakeOptional(1, "id", int32()), | |
| SchemaField::MakeOptional(2, "value", int32())}); | |
| auto count_expr = Expressions::Count("id"); | |
| auto max_expr = Expressions::Max("value"); | |
| auto min_expr = Expressions::Min("value"); | |
| auto count_null_expr = Expressions::CountNull("value"); | |
| auto count_star_expr = Expressions::CountStar(); | |
| auto count_bound = BindAggregate(schema, count_expr); | |
| auto max_bound = BindAggregate(schema, max_expr); | |
| auto min_bound = BindAggregate(schema, min_expr); | |
| auto count_null_bound = BindAggregate(schema, count_null_expr); | |
| auto count_star_bound = BindAggregate(schema, count_star_expr); | |
| std::vector<std::shared_ptr<BoundAggregate>> aggregates{ | |
| count_bound, max_bound, min_bound, count_null_bound, count_star_bound}; | |
| ICEBERG_UNWRAP_OR_FAIL(auto evaluator, AggregateEvaluator::Make(aggregates)); | |
| std::vector<VectorStructLike> rows{ | |
| VectorStructLike({Scalar{int32_t{1}}, Scalar{int32_t{10}}}), | |
| VectorStructLike({Scalar{int32_t{2}}, Scalar{std::monostate{}}}), | |
| VectorStructLike({Scalar{std::monostate{}}, Scalar{int32_t{30}}}), | |
| VectorStructLike({Scalar{int32_t{3}}, Scalar{int32_t{2}}})}; | |
| for (const auto& row : rows) { | |
| ASSERT_TRUE(evaluator->Update(row).has_value()); | |
| } | |
| ICEBERG_UNWRAP_OR_FAIL(auto results, evaluator->GetResults()); | |
| ASSERT_EQ(results.size(), 5); | |
| EXPECT_EQ(std::get<int64_t>(results[0].value()), 3); // count | |
| EXPECT_EQ(std::get<int32_t>(results[1].value()), 30); // max | |
| EXPECT_EQ(std::get<int32_t>(results[2].value()), 2); // min | |
| EXPECT_EQ(std::get<int64_t>(results[3].value()), 1); // count_null | |
| EXPECT_EQ(std::get<int64_t>(results[4].value()), 4); // count_star | |
| } | |
| TEST(AggregateTest, AggregatesFromDataFileMetrics) { | |
| Schema schema({SchemaField::MakeOptional(1, "id", int32()), | |
| SchemaField::MakeOptional(2, "value", int32())}); | |
| auto count_bound = BindAggregate(schema, Expressions::Count("id")); | |
| auto count_null_bound = BindAggregate(schema, Expressions::CountNull("id")); | |
| auto count_star_bound = BindAggregate(schema, Expressions::CountStar()); | |
| auto max_bound = BindAggregate(schema, Expressions::Max("value")); | |
| auto min_bound = BindAggregate(schema, Expressions::Min("value")); | |
| std::vector<std::shared_ptr<BoundAggregate>> aggregates{ | |
| count_bound, count_null_bound, count_star_bound, max_bound, min_bound}; | |
| ICEBERG_UNWRAP_OR_FAIL(auto evaluator, AggregateEvaluator::Make(aggregates)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto lower, Literal::Int(5).Serialize()); | |
| ICEBERG_UNWRAP_OR_FAIL(auto upper, Literal::Int(50).Serialize()); | |
| DataFile file{ | |
| .record_count = 10, | |
| .value_counts = {{1, 10}, {2, 10}}, | |
| .null_value_counts = {{1, 2}, {2, 0}}, | |
| .lower_bounds = {{2, lower}}, | |
| .upper_bounds = {{2, upper}}, | |
| }; | |
| ASSERT_TRUE(evaluator->Update(file).has_value()); | |
| ICEBERG_UNWRAP_OR_FAIL(auto results, evaluator->GetResults()); | |
| ASSERT_EQ(results.size(), aggregates.size()); | |
| EXPECT_EQ(std::get<int64_t>(results[0].value()), 8); // count(id) = 10 - 2 | |
| EXPECT_EQ(std::get<int64_t>(results[1].value()), 2); // count_null(id) | |
| EXPECT_EQ(std::get<int64_t>(results[2].value()), 10); // count_star | |
| EXPECT_EQ(std::get<int32_t>(results[3].value()), 50); // max(value) | |
| EXPECT_EQ(std::get<int32_t>(results[4].value()), 5); // min(value) | |
| } | |
| TEST(AggregateTest, AggregatesFromDataFileMissingMetricsReturnNull) { | |
| Schema schema({SchemaField::MakeOptional(1, "id", int32()), | |
| SchemaField::MakeOptional(2, "value", int32())}); | |
| auto count_bound = BindAggregate(schema, Expressions::Count("id")); | |
| auto count_null_bound = BindAggregate(schema, Expressions::CountNull("id")); | |
| auto count_star_bound = BindAggregate(schema, Expressions::CountStar()); | |
| auto max_bound = BindAggregate(schema, Expressions::Max("value")); | |
| auto min_bound = BindAggregate(schema, Expressions::Min("value")); | |
| std::vector<std::shared_ptr<BoundAggregate>> aggregates{ | |
| count_bound, count_null_bound, count_star_bound, max_bound, min_bound}; | |
| ICEBERG_UNWRAP_OR_FAIL(auto evaluator, AggregateEvaluator::Make(aggregates)); | |
| DataFile file{.record_count = -1}; // missing/invalid | |
| ASSERT_TRUE(evaluator->Update(file).has_value()); | |
| ICEBERG_UNWRAP_OR_FAIL(auto results, evaluator->GetResults()); | |
| ASSERT_EQ(results.size(), aggregates.size()); | |
| for (const auto& literal : results) { | |
| EXPECT_TRUE(literal.IsNull()); | |
| } | |
| } | |
| TEST(AggregateTest, AggregatesFromDataFileWithTransform) { | |
| Schema schema({SchemaField::MakeOptional(1, "id", int32())}); | |
| auto truncate_id = Expressions::Truncate("id", 10); | |
| auto max_bound = BindAggregate(schema, Expressions::Max(truncate_id)); | |
| auto min_bound = BindAggregate(schema, Expressions::Min(truncate_id)); | |
| std::vector<std::shared_ptr<BoundAggregate>> aggregates{max_bound, min_bound}; | |
| ICEBERG_UNWRAP_OR_FAIL(auto evaluator, AggregateEvaluator::Make(aggregates)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto lower, Literal::Int(5).Serialize()); | |
| ICEBERG_UNWRAP_OR_FAIL(auto upper, Literal::Int(23).Serialize()); | |
| DataFile file{ | |
| .record_count = 5, | |
| .value_counts = {{1, 5}}, | |
| .null_value_counts = {{1, 0}}, | |
| .lower_bounds = {{1, lower}}, | |
| .upper_bounds = {{1, upper}}, | |
| }; | |
| ASSERT_TRUE(evaluator->Update(file).has_value()); | |
| ICEBERG_UNWRAP_OR_FAIL(auto results, evaluator->GetResults()); | |
| ASSERT_EQ(results.size(), aggregates.size()); | |
| // Truncate width 10: max(truncate(23)) -> 20, min(truncate(5)) -> 0 | |
| EXPECT_EQ(std::get<int32_t>(results[0].value()), 20); | |
| EXPECT_EQ(std::get<int32_t>(results[1].value()), 0); | |
| EXPECT_TRUE(evaluator->AllAggregatorsValid()); | |
| } | |
| TEST(AggregateTest, DataFileAggregatorParity) { | |
| Schema schema({SchemaField::MakeRequired(1, "id", int32()), | |
| SchemaField::MakeOptional(2, "no_stats", int32()), | |
| SchemaField::MakeOptional(3, "all_nulls", string()), | |
| SchemaField::MakeOptional(4, "some_nulls", string())}); | |
| auto make_bounds = [](int field_id, int32_t lower, int32_t upper) { | |
| std::map<int32_t, std::vector<uint8_t>> lower_bounds; | |
| std::map<int32_t, std::vector<uint8_t>> upper_bounds; | |
| auto lser = Literal::Int(lower).Serialize().value(); | |
| auto user = Literal::Int(upper).Serialize().value(); | |
| lower_bounds.emplace(field_id, std::move(lser)); | |
| upper_bounds.emplace(field_id, std::move(user)); | |
| return std::pair{std::move(lower_bounds), std::move(upper_bounds)}; | |
| }; | |
| auto [b1_lower, b1_upper] = make_bounds(1, 33, 2345); | |
| DataFile file{ | |
| .file_path = "file.avro", | |
| .record_count = 50, | |
| .value_counts = {{1, 50}, {3, 50}, {4, 50}}, | |
| .null_value_counts = {{1, 10}, {3, 50}, {4, 10}}, | |
| .lower_bounds = std::move(b1_lower), | |
| .upper_bounds = std::move(b1_upper), | |
| }; | |
| auto [b2_lower, b2_upper] = make_bounds(1, 33, 100); | |
| DataFile missing_some_nulls_1{ | |
| .file_path = "file_2.avro", | |
| .record_count = 20, | |
| .value_counts = {{1, 20}, {3, 20}}, | |
| .null_value_counts = {{1, 0}, {3, 20}}, | |
| .lower_bounds = std::move(b2_lower), | |
| .upper_bounds = std::move(b2_upper), | |
| }; | |
| auto [b3_lower, b3_upper] = make_bounds(1, -33, 3333); | |
| DataFile missing_some_nulls_2{ | |
| .file_path = "file_3.avro", | |
| .record_count = 20, | |
| .value_counts = {{1, 20}, {3, 20}}, | |
| .null_value_counts = {{1, 20}, {3, 20}}, | |
| .lower_bounds = std::move(b3_lower), | |
| .upper_bounds = std::move(b3_upper), | |
| }; | |
| DataFile missing_some_stats{ | |
| .file_path = "file_missing_stats.avro", | |
| .record_count = 20, | |
| .value_counts = {{1, 20}, {4, 10}}, | |
| }; | |
| auto [b4_lower, b4_upper] = make_bounds(1, -3, 1333); | |
| missing_some_stats.lower_bounds = std::move(b4_lower); | |
| missing_some_stats.upper_bounds = std::move(b4_upper); | |
| DataFile missing_all_optional_stats{ | |
| .file_path = "file_null_stats.avro", | |
| .record_count = 20, | |
| }; | |
| auto run_case = [&](const std::vector<std::shared_ptr<Expression>>& exprs, | |
| const std::vector<DataFile>& files, | |
| const std::vector<std::optional<Scalar>>& expected, | |
| bool expect_all_valid) { | |
| std::vector<std::shared_ptr<BoundAggregate>> aggregates; | |
| aggregates.reserve(exprs.size()); | |
| for (const auto& e : exprs) { | |
| aggregates.emplace_back(BindAggregate(schema, e)); | |
| } | |
| ICEBERG_UNWRAP_OR_FAIL(auto evaluator, AggregateEvaluator::Make(aggregates)); | |
| for (const auto& f : files) { | |
| ASSERT_TRUE(evaluator->Update(f).has_value()); | |
| } | |
| ASSERT_EQ(evaluator->AllAggregatorsValid(), expect_all_valid); | |
| ICEBERG_UNWRAP_OR_FAIL(auto results, evaluator->GetResults()); | |
| ASSERT_EQ(results.size(), expected.size()); | |
| for (size_t i = 0; i < expected.size(); ++i) { | |
| if (!expected[i].has_value()) { | |
| EXPECT_TRUE(results[i].IsNull()); | |
| } else { | |
| const auto& exp = *expected[i]; | |
| const auto& res = results[i].value(); | |
| if (std::holds_alternative<int32_t>(exp)) { | |
| EXPECT_EQ(std::get<int32_t>(res), std::get<int32_t>(exp)); | |
| } else if (std::holds_alternative<int64_t>(exp)) { | |
| EXPECT_EQ(std::get<int64_t>(res), std::get<int64_t>(exp)); | |
| } else { | |
| FAIL() << "Unexpected expected type"; | |
| } | |
| } | |
| } | |
| }; | |
| // testIntAggregate | |
| run_case({Expressions::CountStar(), Expressions::Count("id"), | |
| Expressions::CountNull("id"), Expressions::Max("id"), Expressions::Min("id")}, | |
| {file, missing_some_nulls_1, missing_some_nulls_2}, | |
| {Scalar{int64_t{90}}, Scalar{int64_t{60}}, Scalar{int64_t{30}}, | |
| Scalar{int32_t{3333}}, Scalar{int32_t{-33}}}, | |
| /*expect_all_valid=*/true); | |
| // testAllNulls | |
| run_case({Expressions::CountStar(), Expressions::Count("all_nulls"), | |
| Expressions::CountNull("all_nulls"), Expressions::Max("all_nulls"), | |
| Expressions::Min("all_nulls")}, | |
| {file, missing_some_nulls_1, missing_some_nulls_2}, | |
| {Scalar{int64_t{90}}, Scalar{int64_t{0}}, Scalar{int64_t{90}}, std::nullopt, | |
| std::nullopt}, | |
| /*expect_all_valid=*/true); | |
| // testSomeNulls -> missing null counts for field 4 | |
| run_case({Expressions::CountStar(), Expressions::Count("some_nulls"), | |
| Expressions::CountNull("some_nulls"), Expressions::Max("some_nulls"), | |
| Expressions::Min("some_nulls")}, | |
| {file, missing_some_nulls_1, missing_some_nulls_2}, | |
| {Scalar{int64_t{90}}, std::nullopt, std::nullopt, std::nullopt, std::nullopt}, | |
| /*expect_all_valid=*/false); | |
| // testNoStats -> field 2 has no metrics | |
| run_case({Expressions::CountStar(), Expressions::Count("no_stats"), | |
| Expressions::CountNull("no_stats"), Expressions::Max("no_stats"), | |
| Expressions::Min("no_stats")}, | |
| {file, missing_some_nulls_1, missing_some_nulls_2}, | |
| {Scalar{int64_t{90}}, std::nullopt, std::nullopt, std::nullopt, std::nullopt}, | |
| /*expect_all_valid=*/false); | |
| // testIntAggregateAllMissingStats -> id missing optional stats | |
| run_case({Expressions::CountStar(), Expressions::Count("id"), | |
| Expressions::CountNull("id"), Expressions::Max("id"), Expressions::Min("id")}, | |
| {missing_all_optional_stats}, | |
| {Scalar{int64_t{20}}, std::nullopt, std::nullopt, std::nullopt, std::nullopt}, | |
| /*expect_all_valid=*/false); | |
| // testOptionalColAllMissingStats -> field 2 missing everything | |
| run_case({Expressions::CountStar(), Expressions::Count("no_stats"), | |
| Expressions::CountNull("no_stats"), Expressions::Max("no_stats"), | |
| Expressions::Min("no_stats")}, | |
| {missing_all_optional_stats}, | |
| {Scalar{int64_t{20}}, std::nullopt, std::nullopt, std::nullopt, std::nullopt}, | |
| /*expect_all_valid=*/false); | |
| // testMissingSomeStats -> some_nulls missing null stats entirely | |
| run_case({Expressions::CountStar(), Expressions::Count("some_nulls"), | |
| Expressions::Max("some_nulls"), Expressions::Min("some_nulls")}, | |
| {missing_some_stats}, | |
| {Scalar{int64_t{20}}, std::nullopt, std::nullopt, std::nullopt}, | |
| /*expect_all_valid=*/false); | |
| } | |
| } // namespace iceberg |