| /* |
| * 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/multi_literals_leaf_function.h" |
| |
| #include <cmath> |
| #include <cstdint> |
| #include <memory> |
| #include <vector> |
| |
| #include "arrow/array/array_base.h" |
| #include "arrow/array/array_primitive.h" |
| #include "arrow/compute/api_scalar.h" |
| #include "arrow/compute/exec.h" |
| #include "arrow/datum.h" |
| #include "arrow/memory_pool.h" |
| #include "arrow/type.h" |
| #include "paimon/common/predicate/literal_converter.h" |
| #include "paimon/common/utils/arrow/status_utils.h" |
| #include "paimon/common/utils/checked_cast.h" |
| #include "paimon/data/decimal.h" |
| |
| namespace paimon { |
| namespace { |
| /// The field types `arrow::compute::is_in` can stand in for `Literal::CompareTo`. These are also |
| /// the field types `LiteralConverter::ConvertLiteralsToArray` writes today, and the check keeps one |
| /// it starts writing from reaching `is_in` on its own. |
| /// |
| /// `TIMESTAMP` is parameterized by a time unit, but a timestamp literal settles one itself: the |
| /// finest unit that keeps the values of the literals, which `MakeInValueSet` only probes against |
| /// a column of that very unit. `DECIMAL` is parameterized too, by precision and scale, but a |
| /// decimal literal carries both, and `MakeInValueSet` only probes a column that carries the same |
| /// scale. `FLOAT` and `DOUBLE` agree on every value but NaN, which `MakeInValueSet` keeps off this |
| /// path on its own. |
| bool CanProbeWithIsIn(FieldType field_type) { |
| switch (field_type) { |
| case FieldType::BOOLEAN: |
| case FieldType::TINYINT: |
| case FieldType::SMALLINT: |
| case FieldType::INT: |
| case FieldType::BIGINT: |
| case FieldType::FLOAT: |
| case FieldType::DOUBLE: |
| case FieldType::DATE: |
| case FieldType::STRING: |
| case FieldType::BINARY: |
| case FieldType::DECIMAL: |
| case FieldType::TIMESTAMP: |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| /// Whether `data_type` is the arrow type of a decimal column of `scale`. |
| bool IsDecimalOfScale(const arrow::DataType& data_type, int32_t scale) { |
| return data_type.id() == arrow::Type::DECIMAL128 && |
| checked_cast<const arrow::Decimal128Type&>(data_type).scale() == scale; |
| } |
| |
| /// Whether `data_type` is the arrow type of a timestamp column of `unit` without a time zone. |
| bool IsTimestampOfUnit(const arrow::DataType& data_type, arrow::TimeUnit::type unit) { |
| return data_type.id() == arrow::Type::TIMESTAMP && |
| checked_cast<const arrow::TimestampType&>(data_type).unit() == unit && |
| checked_cast<const arrow::TimestampType&>(data_type).timezone().empty(); |
| } |
| |
| /// Whether `literal` holds a floating point NaN. |
| bool IsNanLiteral(const Literal& literal) { |
| switch (literal.GetType()) { |
| case FieldType::FLOAT: |
| return std::isnan(literal.GetValue<float>()); |
| case FieldType::DOUBLE: |
| return std::isnan(literal.GetValue<double>()); |
| default: |
| return false; |
| } |
| } |
| |
| /// Builds the value set that `arrow::compute::is_in` takes from the literals of an `IN` / `NOT IN` |
| /// predicate. The arrow type comes from the literals themselves, which all share one `FieldType`. |
| /// |
| /// @param negate `false` for `IN`, `true` for `NOT IN`. |
| /// @param data_type The arrow type of the column the predicate is evaluated against, which settles |
| /// whether a decimal or a timestamp value set can be probed against it. |
| /// @param pool The pool the value set is allocated from. |
| /// @return `nullptr` when the literals cannot be probed by `is_in`, which covers the field types |
| /// `CanProbeWithIsIn` rejects, a NaN literal, a decimal column of another scale, a |
| /// timestamp column of another unit or with a time zone, and `NOT IN` holding a null |
| /// literal, which `NotIn::InnerTest` makes false for every row. This never fails. |
| std::shared_ptr<arrow::Array> MakeInValueSet(const std::vector<Literal>& literals, bool negate, |
| const arrow::DataType& data_type, |
| arrow::MemoryPool* pool) { |
| if (literals.empty()) { |
| return nullptr; |
| } |
| // The literals of one predicate share a type, so take it from the first non-null one. When |
| // every literal is null the value set is all nulls, which `is_in` ignores, but it still needs a |
| // type to compare against the column, and a null `Literal` carries its type too. |
| const Literal* typed_literal = &literals.front(); |
| for (const auto& literal : literals) { |
| if (!literal.IsNull()) { |
| typed_literal = &literal; |
| break; |
| } |
| } |
| const FieldType field_type = typed_literal->GetType(); |
| if (!CanProbeWithIsIn(field_type)) { |
| return nullptr; |
| } |
| // A decimal value set carries the precision and the scale of its literals, and `is_in` compares |
| // it against a column of another scale by casting one side to the other. Such a cast fails on a |
| // value that does not fit the other scale, where `Literal::CompareTo` rescales one value at a |
| // time and merely finds no match, so only a column carrying the scale of the literals is |
| // probed. Nothing but null literals leaves no scale to compare and no arrow type to write. |
| if (field_type == FieldType::DECIMAL && |
| (typed_literal->IsNull() || |
| !IsDecimalOfScale(data_type, typed_literal->GetValue<Decimal>().Scale()))) { |
| return nullptr; |
| } |
| // A timestamp value set carries the time unit the values of the literals need, and `is_in` |
| // compares it against a column of another unit by casting one side to the other. Casting to a |
| // coarser unit fails on a value the unit does not keep, casting to a finer one can overflow |
| // int64, where `Literal::CompareTo` merely finds no match, so only a column of that very unit |
| // is probed. A column with a time zone is one as well: `is_in` refuses to compare a zoned |
| // timestamp against an unzoned one, and a literal has no zone to speak of. Nothing but null |
| // literals leaves no unit to compare and no arrow type to write. |
| if (field_type == FieldType::TIMESTAMP && |
| (typed_literal->IsNull() || |
| !IsTimestampOfUnit(data_type, LiteralConverter::MinRequiredTimeUnit(literals)))) { |
| return nullptr; |
| } |
| for (const auto& literal : literals) { |
| // A literal typed differently makes `Literal::CompareTo` fail, keep that on the row by row |
| // path. |
| if (!literal.IsNull() && literal.GetType() != field_type) { |
| return nullptr; |
| } |
| // `is_in` hashes the raw bits of a float, so a NaN literal would only match the column NaNs |
| // carrying the very same bit pattern, while `FieldsComparator::CompareFloatingPoint` makes |
| // every NaN equal. Keep a NaN literal on the row by row path. With none in the value set |
| // the two agree, because a column NaN then matches no literal either way. |
| if (!literal.IsNull() && IsNanLiteral(literal)) { |
| return nullptr; |
| } |
| // `NotIn::InnerTest` returns false as soon as it meets a null literal, so no row can match |
| // and there is nothing worth building. |
| if (negate && literal.IsNull()) { |
| return nullptr; |
| } |
| } |
| Result<std::shared_ptr<arrow::Array>> value_set = |
| LiteralConverter::ConvertLiteralsToArray(field_type, literals, pool); |
| // A failure only says the value set is not there, and the row by row path still is. Decimal |
| // literals of mixed precision and scale end up here, anything else is an arrow failure. |
| if (!value_set.ok()) { |
| return nullptr; |
| } |
| return std::move(value_set).value(); |
| } |
| |
| /// Probes every non-null row of `array` against `value_set`. |
| /// |
| /// @param negate `false` for `IN` semantics, `true` for `NOT IN` semantics. |
| /// @param pool The pool the match bitmap `is_in` writes is allocated from. |
| /// @return One entry per row, with the null rows left at 0 because `IN` and `NOT IN` are both false |
| /// on null. |
| /// |
| /// `is_in` resolves the comparison itself: it decodes a dictionary column, and promotes both sides |
| /// to their common type when the column is read as a wider or narrower arrow type than the one |
| /// `value_set` was built with. 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>> ProbeInValueSet(const arrow::Array& array, const arrow::Array& value_set, |
| bool negate, arrow::MemoryPool* pool) { |
| // `EMIT_NULL` ignores the nulls of the value set and turns a null input into a null output, so |
| // the validity of `matches` marks exactly the rows that `In` / `NotIn` consider null. That also |
| // covers a dictionary column, whose null rows come either from the indices or from a null |
| // dictionary value once `is_in` decodes it. |
| arrow::compute::SetLookupOptions options(value_set.data(), |
| arrow::compute::SetLookupOptions::EMIT_NULL); |
| arrow::compute::ExecContext exec_context(pool); |
| PAIMON_ASSIGN_OR_RAISE_FROM_ARROW( |
| arrow::Datum matches, arrow::compute::IsIn(arrow::Datum(array), options, &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); |
| std::vector<char> is_valid(matched.length(), 0); |
| for (int64_t i = 0; i < matched.length(); i++) { |
| if (matched.IsNull(i)) { |
| // `IN` and `NOT IN` are both false on a null value, leave the row at 0. |
| continue; |
| } |
| is_valid[i] = static_cast<char>(matched.Value(i) != negate); |
| } |
| return is_valid; |
| } |
| } // namespace |
| |
| Result<std::vector<char>> MultiLiteralsLeafFunction::Test(const arrow::Array& array, |
| const std::vector<Literal>& literals, |
| arrow::MemoryPool* pool) const { |
| const Function::Type type = GetType(); |
| // `In` and `NotIn` are the only subclasses today. The check keeps a future one off this path, |
| // because `MakeInValueSet` only looks at the literals and would silently give it `IN` |
| // semantics. |
| if (type == Function::Type::IN || type == Function::Type::NOT_IN) { |
| const bool negate = type == Function::Type::NOT_IN; |
| std::shared_ptr<arrow::Array> value_set = |
| MakeInValueSet(literals, negate, *array.type(), pool); |
| if (value_set != nullptr) { |
| return ProbeInValueSet(array, *value_set, negate, pool); |
| } |
| } |
| |
| // Materializing the column into `Literal` objects costs one heap allocation per row and then |
| // every row scans the literals linearly, so this only runs when `is_in` cannot probe them. |
| PAIMON_ASSIGN_OR_RAISE(std::vector<Literal> array_values, |
| LiteralConverter::ConvertLiteralsFromArray(array, /*own_data=*/false)); |
| std::vector<char> is_valid(array.length(), 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)); |
| } |
| } |
| return is_valid; |
| } |
| |
| } // namespace paimon |