blob: 8e42fc2cc5e51efea665a676f38421f02032a8dd [file]
// 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 <gtest/gtest.h>
#include <limits>
#include <memory>
#include <optional>
#include <vector>
#include "core/column/column_array.h"
#include "core/column/column_nullable.h"
#include "core/column/column_string.h"
#include "core/column/column_vector.h"
#include "core/data_type/data_type_array.h"
#include "core/data_type/data_type_nullable.h"
#include "core/data_type/data_type_number.h"
#include "core/string_buffer.hpp"
#include "exprs/aggregate/aggregate_function_percentile.h"
#include "exprs/aggregate/aggregate_function_simple_factory.h"
#include "util/tdigest.h"
namespace doris {
namespace {
AggregateFunctionPtr create_percentile_approx_array_function(bool has_compression) {
DataTypes argument_types {
std::make_shared<DataTypeFloat64>(),
std::make_shared<DataTypeArray>(make_nullable(std::make_shared<DataTypeFloat64>()))};
if (has_compression) {
argument_types.push_back(std::make_shared<DataTypeFloat64>());
}
auto result_type =
std::make_shared<DataTypeArray>(make_nullable(std::make_shared<DataTypeFloat64>()));
return AggregateFunctionSimpleFactory::instance().get(
"percentile_approx_array", argument_types, result_type, false,
BeExecVersionManager::get_newest_version(), {.column_names = {}});
}
MutableColumnPtr create_values(const std::vector<double>& values) {
auto column = ColumnFloat64::create();
for (double value : values) {
column->insert_value(value);
}
return column;
}
MutableColumnPtr create_quantiles(const std::vector<double>& quantiles, size_t rows,
std::optional<size_t> null_index = std::nullopt) {
auto nested_column = ColumnFloat64::create();
auto null_map = ColumnUInt8::create();
auto offsets = ColumnArray::ColumnOffsets::create();
for (size_t row = 0; row < rows; ++row) {
for (size_t i = 0; i < quantiles.size(); ++i) {
nested_column->insert_value(quantiles[i]);
null_map->insert_value(null_index.has_value() && *null_index == i);
}
offsets->insert_value((row + 1) * quantiles.size());
}
return ColumnArray::create(
ColumnNullable::create(std::move(nested_column), std::move(null_map)),
std::move(offsets));
}
std::vector<double> read_result(const AggregateFunctionPtr& function, AggregateDataPtr place) {
auto result_column = ColumnArray::create(
ColumnNullable::create(ColumnFloat64::create(), ColumnUInt8::create()),
ColumnArray::ColumnOffsets::create());
function->insert_result_into(place, *result_column);
const auto& nullable_data = assert_cast<const ColumnNullable&>(result_column->get_data());
const auto& data = assert_cast<const ColumnFloat64&>(nullable_data.get_nested_column());
return {data.get_data().begin(), data.get_data().end()};
}
std::vector<double> expected_quantiles(const std::vector<double>& values,
const std::vector<double>& quantiles, float compression) {
TDigest digest(compression);
for (double value : values) {
digest.add(value);
}
std::vector<double> result;
result.reserve(quantiles.size());
for (double quantile : quantiles) {
result.push_back(digest.quantile(quantile));
}
return result;
}
void expect_results_equal(const std::vector<double>& actual, const std::vector<double>& expected) {
ASSERT_EQ(actual.size(), expected.size());
for (size_t i = 0; i < actual.size(); ++i) {
EXPECT_DOUBLE_EQ(actual[i], expected[i]);
}
}
} // namespace
TEST(AggregateFunctionPercentileApproxArrayTest, AddAndBatchPaths) {
const std::vector<double> values {1, 2, 3, 4, 5, 100};
const std::vector<double> quantiles {0.9, 0.0, 0.5, 0.5, 1.0};
auto function = create_percentile_approx_array_function(false);
ASSERT_NE(function, nullptr);
auto value_column = create_values(values);
auto quantile_column = create_quantiles(quantiles, values.size());
const IColumn* columns[] = {value_column.get(), quantile_column.get()};
Arena arena;
std::unique_ptr<char[]> row_memory(new char[function->size_of_data()]);
AggregateDataPtr row_place = row_memory.get();
function->create(row_place);
for (size_t i = 0; i < values.size(); ++i) {
function->add(row_place, columns, i, arena);
}
std::unique_ptr<char[]> batch_memory(new char[function->size_of_data()]);
AggregateDataPtr batch_place = batch_memory.get();
function->create(batch_place);
function->add_batch_single_place(values.size(), batch_place, columns, arena);
std::unique_ptr<char[]> range_memory(new char[function->size_of_data()]);
AggregateDataPtr range_place = range_memory.get();
function->create(range_place);
function->add_batch_range(1, 4, range_place, columns, arena, false);
const auto expected = expected_quantiles(values, quantiles, 10000);
expect_results_equal(read_result(function, row_place), expected);
expect_results_equal(read_result(function, batch_place), expected);
expect_results_equal(read_result(function, range_place),
expected_quantiles({2, 3, 4, 5}, quantiles, 10000));
function->destroy(row_place);
function->destroy(batch_place);
function->destroy(range_place);
}
TEST(AggregateFunctionPercentileApproxArrayTest, CompressionSerializationAndMerge) {
const std::vector<double> values {5, 10, 15, 20, 25, 30};
const std::vector<double> quantiles {0.25, 0.5, 0.75};
auto function = create_percentile_approx_array_function(true);
ASSERT_NE(function, nullptr);
auto value_column = create_values(values);
auto quantile_column = create_quantiles(quantiles, values.size());
auto compression_column = ColumnFloat64::create();
compression_column->insert_value(2048);
const IColumn* columns[] = {value_column.get(), quantile_column.get(),
compression_column.get()};
Arena arena;
std::unique_ptr<char[]> source_memory(new char[function->size_of_data()]);
AggregateDataPtr source_place = source_memory.get();
function->create(source_place);
function->add_batch_single_place(values.size(), source_place, columns, arena);
ColumnString serialized_column;
VectorBufferWriter writer(serialized_column);
function->serialize(source_place, writer);
writer.commit();
std::unique_ptr<char[]> restored_memory(new char[function->size_of_data()]);
AggregateDataPtr restored_place = restored_memory.get();
function->create(restored_place);
VectorBufferReader reader(serialized_column.get_data_at(0));
function->deserialize(restored_place, reader, arena);
std::unique_ptr<char[]> merged_memory(new char[function->size_of_data()]);
AggregateDataPtr merged_place = merged_memory.get();
function->create(merged_place);
function->merge(merged_place, restored_place, arena);
const auto expected = expected_quantiles(values, quantiles, 2048);
expect_results_equal(read_result(function, restored_place), expected);
expect_results_equal(read_result(function, merged_place), expected);
function->destroy(source_place);
function->destroy(restored_place);
function->destroy(merged_place);
}
TEST(AggregateFunctionPercentileApproxArrayTest, MergeCompatibleInitializedStates) {
const std::vector<double> left_values {5, 10, 15};
const std::vector<double> right_values {20, 25, 30};
const std::vector<double> quantiles {0.25, 0.5, 0.75};
auto function = create_percentile_approx_array_function(true);
ASSERT_NE(function, nullptr);
Arena arena;
auto create_state = [&](const std::vector<double>& values) {
auto value_column = create_values(values);
auto quantile_column = create_quantiles(quantiles, values.size());
auto compression_column = ColumnFloat64::create();
compression_column->insert_value(2048);
const IColumn* columns[] = {value_column.get(), quantile_column.get(),
compression_column.get()};
std::unique_ptr<char[]> memory(new char[function->size_of_data()]);
function->create(memory.get());
function->add_batch_single_place(values.size(), memory.get(), columns, arena);
return memory;
};
auto left = create_state(left_values);
auto right = create_state(right_values);
function->merge(left.get(), right.get(), arena);
std::vector<double> all_values = left_values;
all_values.insert(all_values.end(), right_values.begin(), right_values.end());
expect_results_equal(read_result(function, left.get()),
expected_quantiles(all_values, quantiles, 2048));
function->destroy(left.get());
function->destroy(right.get());
}
TEST(AggregateFunctionPercentileApproxArrayTest, EmptyQuantilesAndInvalidQuantile) {
auto function = create_percentile_approx_array_function(false);
ASSERT_NE(function, nullptr);
Arena arena;
auto value_column = create_values({1, 2, 3});
auto empty_quantile_column = create_quantiles({}, 3);
const IColumn* empty_columns[] = {value_column.get(), empty_quantile_column.get()};
std::unique_ptr<char[]> empty_memory(new char[function->size_of_data()]);
AggregateDataPtr empty_place = empty_memory.get();
function->create(empty_place);
function->add_batch_single_place(3, empty_place, empty_columns, arena);
EXPECT_TRUE(read_result(function, empty_place).empty());
const auto& empty_state = *reinterpret_cast<const PercentileApproxArrayState*>(empty_place);
EXPECT_TRUE(empty_state.init_flag);
EXPECT_EQ(empty_state.digest.get(), nullptr);
ColumnString serialized_column;
VectorBufferWriter writer(serialized_column);
function->serialize(empty_place, writer);
writer.commit();
std::unique_ptr<char[]> restored_memory(new char[function->size_of_data()]);
AggregateDataPtr restored_place = restored_memory.get();
function->create(restored_place);
VectorBufferReader reader(serialized_column.get_data_at(0));
function->deserialize(restored_place, reader, arena);
const auto& restored_state =
*reinterpret_cast<const PercentileApproxArrayState*>(restored_place);
EXPECT_TRUE(restored_state.init_flag);
EXPECT_EQ(restored_state.digest.get(), nullptr);
EXPECT_TRUE(read_result(function, restored_place).empty());
std::unique_ptr<char[]> merged_memory(new char[function->size_of_data()]);
AggregateDataPtr merged_place = merged_memory.get();
function->create(merged_place);
function->merge(merged_place, restored_place, arena);
const auto& merged_state = *reinterpret_cast<const PercentileApproxArrayState*>(merged_place);
EXPECT_TRUE(merged_state.init_flag);
EXPECT_EQ(merged_state.digest.get(), nullptr);
EXPECT_TRUE(read_result(function, merged_place).empty());
auto invalid_quantile_column = create_quantiles({0.5, 0.9}, 3, 1);
const IColumn* invalid_columns[] = {value_column.get(), invalid_quantile_column.get()};
std::unique_ptr<char[]> invalid_memory(new char[function->size_of_data()]);
AggregateDataPtr invalid_place = invalid_memory.get();
function->create(invalid_place);
EXPECT_THROW(function->add_batch_single_place(3, invalid_place, invalid_columns, arena),
Exception);
function->destroy(empty_place);
function->destroy(restored_place);
function->destroy(merged_place);
function->destroy(invalid_place);
}
TEST(AggregateFunctionPercentileApproxArrayTest, RejectsIncompatibleStates) {
auto function = create_percentile_approx_array_function(true);
ASSERT_NE(function, nullptr);
Arena arena;
auto create_state = [&](const std::vector<double>& quantiles, double compression) {
auto value_column = create_values({1, 2, 3});
auto quantile_column = create_quantiles(quantiles, 3);
auto compression_column = ColumnFloat64::create();
compression_column->insert_value(compression);
const IColumn* columns[] = {value_column.get(), quantile_column.get(),
compression_column.get()};
std::unique_ptr<char[]> memory(new char[function->size_of_data()]);
function->create(memory.get());
function->add_batch_single_place(3, memory.get(), columns, arena);
return memory;
};
auto destination = create_state({0.5}, 2048);
auto different_quantiles = create_state({0.9}, 2048);
auto different_compression = create_state({0.5}, 10000);
EXPECT_THROW(function->merge(destination.get(), different_quantiles.get(), arena), Exception);
EXPECT_THROW(function->merge(destination.get(), different_compression.get(), arena), Exception);
function->destroy(destination.get());
function->destroy(different_quantiles.get());
function->destroy(different_compression.get());
}
TEST(AggregateFunctionPercentileApproxArrayTest, HandlesNonFiniteParameters) {
const std::vector<double> non_finite_values {std::numeric_limits<double>::quiet_NaN(),
std::numeric_limits<double>::infinity(),
-std::numeric_limits<double>::infinity()};
auto value_column = create_values({1, 2, 3});
Arena arena;
auto function_without_compression = create_percentile_approx_array_function(false);
ASSERT_NE(function_without_compression, nullptr);
for (double quantile : non_finite_values) {
auto quantile_column = create_quantiles({quantile}, 3);
const IColumn* columns[] = {value_column.get(), quantile_column.get()};
std::unique_ptr<char[]> memory(new char[function_without_compression->size_of_data()]);
function_without_compression->create(memory.get());
EXPECT_THROW(function_without_compression->add_batch_single_place(3, memory.get(), columns,
arena),
Exception);
function_without_compression->destroy(memory.get());
}
auto function_with_compression = create_percentile_approx_array_function(true);
ASSERT_NE(function_with_compression, nullptr);
auto quantile_column = create_quantiles({0.5}, 3);
const auto expected = expected_quantiles({1, 2, 3}, {0.5}, 10000);
for (double compression : non_finite_values) {
auto compression_column = ColumnFloat64::create();
compression_column->insert_value(compression);
const IColumn* columns[] = {value_column.get(), quantile_column.get(),
compression_column.get()};
std::unique_ptr<char[]> memory(new char[function_with_compression->size_of_data()]);
function_with_compression->create(memory.get());
function_with_compression->add_batch_single_place(3, memory.get(), columns, arena);
const auto& state = *reinterpret_cast<const PercentileApproxArrayState*>(memory.get());
EXPECT_FLOAT_EQ(state.compressions, 10000);
expect_results_equal(read_result(function_with_compression, memory.get()), expected);
function_with_compression->destroy(memory.get());
}
}
} // namespace doris