blob: 7aa957590298a3dd0fc25a64216dcdd98b33a9f8 [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 "paimon/common/predicate/null_false_leaf_binary_function.h"
#include <cstdint>
#include <memory>
#include <optional>
#include <vector>
#include "arrow/array/array_base.h"
#include "arrow/array/array_primitive.h"
#include "arrow/compute/exec.h"
#include "arrow/datum.h"
#include "arrow/memory_pool.h"
#include "arrow/result.h"
#include "arrow/scalar.h"
#include "arrow/type.h"
#include "paimon/common/predicate/literal_converter.h"
#include "paimon/common/utils/arrow/arrow_utils.h"
#include "paimon/common/utils/arrow/status_utils.h"
#include "paimon/common/utils/checked_cast.h"
#include "paimon/common/utils/field_type_utils.h"
#include "paimon/defs.h"
#include "paimon/status.h"
namespace paimon {
namespace {
/// The `arrow::compute` kernel that compares a column against a scalar the way `Test(field,
/// literal)` compares two `Literal`s, or `nullptr` for a function that is not a comparison.
///
/// `STARTS_WITH`, `ENDS_WITH`, `CONTAINS` and `LIKE` are `NullFalseLeafBinaryFunction`s too, but
/// they match a pattern instead of ordering two values, so they are not here and keep the row by
/// row path.
const char* ComparisonKernel(Function::Type type) {
switch (type) {
case Function::Type::EQUAL:
return "equal";
case Function::Type::NOT_EQUAL:
return "not_equal";
case Function::Type::LESS_THAN:
return "less";
case Function::Type::LESS_OR_EQUAL:
return "less_equal";
case Function::Type::GREATER_THAN:
return "greater";
case Function::Type::GREATER_OR_EQUAL:
return "greater_equal";
default:
return nullptr;
}
}
/// The field types one comparison kernel can stand in for `Literal::CompareTo`.
///
/// `FLOAT` and `DOUBLE` are not here: `FieldsComparator::CompareFloatingPoint` orders
/// `-0.0 < +0.0` and makes every NaN equal to every NaN, where a kernel follows IEEE 754 and says
/// `-0.0 == +0.0` and that no NaN compares to anything. Unlike a NaN literal, which `IN` keeps off
/// its own fast path by looking at the literals, that divergence is a property of the column, so no
/// literal can be checked for it and a float column keeps the row by row path.
bool CanCompareWithKernel(FieldType field_type) {
switch (field_type) {
case FieldType::BOOLEAN:
case FieldType::TINYINT:
case FieldType::SMALLINT:
case FieldType::INT:
case FieldType::BIGINT:
case FieldType::DATE:
case FieldType::STRING:
case FieldType::BINARY:
case FieldType::DECIMAL:
case FieldType::TIMESTAMP:
return true;
default:
return false;
}
}
/// The `FieldType` the row by row path reads the values of `array` as, which is the one
/// `LiteralConverter::ConvertLiteralsFromArray` gives them, or `std::nullopt` for a layout that
/// conversion rejects and this therefore has to leave to it to report.
std::optional<FieldType> ArrayFieldType(const arrow::Array& array) {
const std::shared_ptr<arrow::DataType>& type = array.type();
if (type->id() == arrow::Type::DICTIONARY) {
const auto& dict_type = checked_cast<const arrow::DictionaryType&>(*type);
const arrow::Type::type value_type = dict_type.value_type()->id();
const arrow::Type::type index_type = dict_type.index_type()->id();
if ((value_type == arrow::Type::STRING || value_type == arrow::Type::BINARY) &&
index_type == arrow::Type::INT32) {
return value_type == arrow::Type::STRING ? FieldType::STRING : FieldType::BINARY;
}
if ((value_type == arrow::Type::LARGE_STRING || value_type == arrow::Type::LARGE_BINARY) &&
index_type == arrow::Type::INT64) {
return value_type == arrow::Type::LARGE_STRING ? FieldType::STRING : FieldType::BINARY;
}
return std::nullopt;
}
Result<FieldType> field_type = FieldTypeUtils::ConvertToFieldType(type->id());
if (!field_type.ok()) {
return std::nullopt;
}
return field_type.value();
}
/// Whether the column and the scalar the literal was written to can be compared without arrow
/// casting one side to a type that `Literal::CompareTo` does not compare by.
///
/// A kernel resolves a type difference by casting, and a cast fails on a value the target type does
/// not keep, where `Decimal::CompareTo` rescales two decimals and compares them by value and
/// `Literal::CompareTo` compares two timestamps by the instant they name and merely finds the
/// answer. A decimal column of another scale than the literal and a timestamp column of another
/// unit or with a time zone therefore keep the row by row path.
///
/// Every other difference is one arrow resolves losslessly: a kernel decodes a dictionary column to
/// its values, a string column of another width compares byte by byte either way, and a decimal
/// column of another precision carrying the scale of the literal is widened without losing a digit.
/// The two `FieldType`s are already guarded to be the same one, so no numeric column reaches a
/// kernel of another width.
bool AgreesWithScalar(const arrow::DataType& column_type, const arrow::DataType& scalar_type) {
switch (column_type.id()) {
case arrow::Type::DECIMAL128:
return scalar_type.id() == arrow::Type::DECIMAL128 &&
checked_cast<const arrow::Decimal128Type&>(column_type).scale() ==
checked_cast<const arrow::Decimal128Type&>(scalar_type).scale();
case arrow::Type::TIMESTAMP:
return scalar_type.id() == arrow::Type::TIMESTAMP &&
checked_cast<const arrow::TimestampType&>(column_type).unit() ==
checked_cast<const arrow::TimestampType&>(scalar_type).unit() &&
checked_cast<const arrow::TimestampType&>(column_type).timezone().empty();
default:
return true;
}
}
/// Writes `literal` to the one element scalar a comparison kernel compares the column against.
///
/// @param field_type The field type of the literal, which picks the arrow type it is written with.
/// @param column_type The arrow type of the column the predicate is evaluated against, which
/// settles whether a decimal or a timestamp scalar can be compared against it.
/// @param pool The pool the scalar is allocated from.
/// @return `nullptr` when the literal cannot be compared by a kernel, which covers a decimal column
/// of another scale and a timestamp column of another unit or with a time zone. This never
/// fails.
std::shared_ptr<arrow::Scalar> MakeComparisonScalar(const Literal& literal, FieldType field_type,
const arrow::DataType& column_type,
arrow::MemoryPool* pool) {
// A `NullFalseLeafBinaryFunction` compares one literal at a time, so there is exactly one to
// write. `ConvertLiteralsToArray` is what `IN` writes its value set with, and it settles the
// arrow type of the scalar the same way for both.
Result<std::shared_ptr<arrow::Array>> one =
LiteralConverter::ConvertLiteralsToArray(field_type, {literal}, pool);
// A failure only says the scalar is not there, and the row by row path still is.
if (!one.ok()) {
return nullptr;
}
arrow::Result<std::shared_ptr<arrow::Scalar>> scalar = one.value()->GetScalar(0);
if (!scalar.ok()) {
return nullptr;
}
const std::shared_ptr<arrow::Scalar>& value = scalar.ValueOrDie();
if (!AgreesWithScalar(column_type, *value->type)) {
return nullptr;
}
return value;
}
/// Compares every row of `array` against `literal` with the kernel `kernel_name`.
///
/// @param pool The pool the boolean bitmap the kernel writes is allocated from.
/// @return One entry per row, with the null rows left at 0 because a comparison is false on null.
///
/// The kernel resolves the comparison itself: it decodes a dictionary column and emits a null for a
/// null row. It fails when the two types have no common type at all, which only happens when the
/// field type disagrees with the column the predicate is evaluated against.
Result<std::vector<char>> ProbeComparison(const arrow::Array& array, const char* kernel_name,
const std::shared_ptr<arrow::Scalar>& literal,
arrow::MemoryPool* pool) {
arrow::compute::ExecContext exec_context(pool);
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
arrow::Datum matches,
arrow::compute::CallFunction(kernel_name, {arrow::Datum(array), arrow::Datum(literal)},
/*options=*/nullptr, &exec_context));
// `make_array` hands out a new `shared_ptr` that owns the array, so it has to be held for as
// long as the array is read. Binding a reference straight to what it points at would drop the
// last owner at the end of the statement and leave that reference dangling.
std::shared_ptr<arrow::Array> matches_array = matches.make_array();
const auto& matched = checked_cast<const arrow::BooleanArray&>(*matches_array);
// A kernel emits a null for a null row, and every `NullFalseLeafBinaryFunction` is false on
// one, so the null rows unpack to 0 rather than to a value they do not hold.
return ArrowUtils::UnpackBooleansToBytes(matched, /*negate=*/false);
}
} // namespace
Result<std::vector<char>> NullFalseLeafBinaryFunction::Test(const arrow::Array& array,
const std::vector<Literal>& literals,
arrow::MemoryPool* pool) const {
if (literals.size() < LITERAL_LIMIT) {
return Status::Invalid("NullFalseLeafBinaryFunction needs single literal for field");
}
std::vector<char> is_valid(array.length(), false);
if (literals[0].IsNull()) {
return is_valid;
}
const char* kernel_name = ComparisonKernel(GetType());
if (kernel_name != nullptr) {
const std::optional<FieldType> field_type = ArrayFieldType(array);
// A literal typed differently from the column makes `Literal::CompareTo` fail, so it keeps
// the row by row path that reports it instead of reaching a kernel that would cast one side
// to the other and compare by chance.
if (field_type != std::nullopt && *field_type == literals[0].GetType() &&
CanCompareWithKernel(*field_type)) {
std::shared_ptr<arrow::Scalar> scalar =
MakeComparisonScalar(literals[0], *field_type, *array.type(), pool);
if (scalar != nullptr) {
return ProbeComparison(array, kernel_name, scalar, pool);
}
}
}
// Materializing the column into `Literal` objects costs one heap allocation and one hash of the
// value per row, so this only runs when no kernel can compare the column against the literal.
PAIMON_ASSIGN_OR_RAISE(std::vector<Literal> array_values,
LiteralConverter::ConvertLiteralsFromArray(array, /*own_data=*/false));
for (int64_t i = 0; i < array.length(); i++) {
if (!array.IsNull(i)) {
PAIMON_ASSIGN_OR_RAISE(is_valid[i], Test(array_values[i], literals[0]));
}
}
return is_valid;
}
} // namespace paimon