| /* | |
| * 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/sanitize_expression.h" | |
| #include <chrono> | |
| #include <cmath> | |
| #include <cstdint> | |
| #include <format> | |
| #include <limits> | |
| #include <regex> | |
| #include <string> | |
| #include <string_view> | |
| #include <utility> | |
| #include <vector> | |
| #include "iceberg/expression/binder.h" | |
| #include "iceberg/expression/literal.h" | |
| #include "iceberg/expression/predicate.h" | |
| #include "iceberg/expression/term.h" | |
| #include "iceberg/transform.h" | |
| #include "iceberg/type.h" | |
| #include "iceberg/util/bucket_util.h" | |
| #include "iceberg/util/checked_cast.h" | |
| #include "iceberg/util/temporal_util.h" | |
| namespace iceberg { | |
| namespace { | |
| std::string SanitizeDate(int32_t days, int32_t today) { | |
| std::string is_past = today > days ? "ago" : "from-now"; | |
| uint32_t diff = today > days ? static_cast<uint32_t>(static_cast<uint32_t>(today) - | |
| static_cast<uint32_t>(days)) | |
| : static_cast<uint32_t>(static_cast<uint32_t>(days) - | |
| static_cast<uint32_t>(today)); | |
| if (diff == 0) { | |
| return "(date-today)"; | |
| } else if (diff < 90) { | |
| return "(date-" + std::to_string(diff) + "-days-" + is_past + ")"; | |
| } | |
| return "(date)"; | |
| } | |
| std::string SanitizeTimestamp(int64_t micros, int64_t now) { | |
| constexpr int64_t kMicrosPerHour = 60LL * 60LL * 1'000'000LL; | |
| constexpr int64_t kFiveMinutesInMicros = 5LL * 60LL * 1'000'000LL; | |
| constexpr int64_t kThreeDaysInHours = 3LL * 24LL; | |
| constexpr int64_t kNinetyDaysInHours = 90LL * 24LL; | |
| std::string is_past = now > micros ? "ago" : "from-now"; | |
| uint64_t diff = now > micros ? static_cast<uint64_t>(static_cast<uint64_t>(now) - | |
| static_cast<uint64_t>(micros)) | |
| : static_cast<uint64_t>(static_cast<uint64_t>(micros) - | |
| static_cast<uint64_t>(now)); | |
| if (diff < kFiveMinutesInMicros) { | |
| return "(timestamp-about-now)"; | |
| } | |
| int64_t hours = diff / kMicrosPerHour; | |
| if (hours <= kThreeDaysInHours) { | |
| return "(timestamp-" + std::to_string(hours) + "-hours-" + is_past + ")"; | |
| } else if (hours < kNinetyDaysInHours) { | |
| int64_t days = hours / 24; | |
| return "(timestamp-" + std::to_string(days) + "-days-" + is_past + ")"; | |
| } | |
| return "(timestamp)"; | |
| } | |
| std::string SanitizeNumber(double value, std::string_view type) { | |
| if (!std::isfinite(value)) { | |
| return std::format("({})", type); | |
| } | |
| int32_t num_digits = | |
| value == 0 ? 1 : static_cast<int32_t>(std::log10(std::abs(value))) + 1; | |
| return std::format("({}-digit-{})", num_digits, type); | |
| } | |
| Result<std::string> SanitizeSimpleString(std::string_view value) { | |
| ICEBERG_ASSIGN_OR_RAISE(auto hash, | |
| BucketUtils::BucketIndex(Literal::String(std::string(value)), | |
| std::numeric_limits<int32_t>::max())); | |
| return std::format("(hash-{:08x})", hash); | |
| } | |
| Result<std::string> SanitizeString(std::string_view value, int64_t now, int32_t today) { | |
| static const std::regex kDate(R"(\d{4}-\d{2}-\d{2})"); | |
| static const std::regex kTime(R"(\d{2}:\d{2}(:\d{2}(.\d{1,9})?)?)"); | |
| static const std::regex kTimestamp( | |
| R"(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(.\d{1,9})?)?)"); | |
| static const std::regex kTimestampTz( | |
| R"(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(.\d{1,9})?)?([-+]\d{2}:\d{2}|Z))"); | |
| static const std::regex kTimestampNs( | |
| R"(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(.\d{7,9})?)?)"); | |
| static const std::regex kTimestampTzNs( | |
| R"(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(.\d{7,9})?)?([-+]\d{2}:\d{2}|Z))"); | |
| try { | |
| if (std::regex_match(value.begin(), value.end(), kDate)) { | |
| auto days = TemporalUtils::ParseDay(value); | |
| return days.has_value() ? SanitizeDate(*days, today) : SanitizeSimpleString(value); | |
| } | |
| if (std::regex_match(value.begin(), value.end(), kTimestampNs)) { | |
| auto nanos = TemporalUtils::ParseTimestampNs(value); | |
| return nanos.has_value() | |
| ? SanitizeTimestamp(TemporalUtils::NanosToMicros(*nanos), now) | |
| : SanitizeSimpleString(value); | |
| } | |
| if (std::regex_match(value.begin(), value.end(), kTimestampTzNs)) { | |
| auto nanos = TemporalUtils::ParseTimestampNsWithZone(value); | |
| return nanos.has_value() | |
| ? SanitizeTimestamp(TemporalUtils::NanosToMicros(*nanos), now) | |
| : SanitizeSimpleString(value); | |
| } | |
| if (std::regex_match(value.begin(), value.end(), kTimestamp)) { | |
| auto micros = TemporalUtils::ParseTimestamp(value); | |
| return micros.has_value() ? SanitizeTimestamp(*micros, now) | |
| : SanitizeSimpleString(value); | |
| } | |
| if (std::regex_match(value.begin(), value.end(), kTimestampTz)) { | |
| auto micros = TemporalUtils::ParseTimestampWithZone(value); | |
| return micros.has_value() ? SanitizeTimestamp(*micros, now) | |
| : SanitizeSimpleString(value); | |
| } | |
| if (std::regex_match(value.begin(), value.end(), kTime)) { | |
| return std::string("(time)"); | |
| } | |
| return SanitizeSimpleString(value); | |
| } catch (const std::exception&) { | |
| return SanitizeSimpleString(value); | |
| } | |
| } | |
| Result<std::string> SanitizePlaceholder(const Literal& literal, int64_t now, | |
| int32_t today) { | |
| if (literal.IsNull()) { | |
| return std::string("(null)"); | |
| } | |
| const auto& value = literal.value(); | |
| switch (literal.type()->type_id()) { | |
| case TypeId::kString: | |
| return SanitizeString(std::get<std::string>(value), now, today); | |
| case TypeId::kDate: | |
| return SanitizeDate(std::get<int32_t>(value), today); | |
| case TypeId::kTimestamp: | |
| case TypeId::kTimestampTz: | |
| return SanitizeTimestamp(std::get<int64_t>(value), now); | |
| case TypeId::kTimestampNs: | |
| case TypeId::kTimestampTzNs: | |
| return SanitizeTimestamp(TemporalUtils::NanosToMicros(std::get<int64_t>(value)), | |
| now); | |
| case TypeId::kTime: | |
| return std::string("(time)"); | |
| case TypeId::kInt: | |
| return SanitizeNumber(std::get<int32_t>(value), "int"); | |
| case TypeId::kLong: | |
| return SanitizeNumber(static_cast<double>(std::get<int64_t>(value)), "int"); | |
| case TypeId::kFloat: | |
| return SanitizeNumber(std::get<float>(value), "float"); | |
| case TypeId::kDouble: | |
| return SanitizeNumber(std::get<double>(value), "float"); | |
| case TypeId::kBinary: | |
| case TypeId::kFixed: | |
| return SanitizeSimpleString(literal.ToString()); | |
| default: | |
| return SanitizeSimpleString(literal.ToString()); | |
| } | |
| } | |
| Result<Literal> SanitizeLiteral(const Literal& literal, int64_t now, int32_t today) { | |
| ICEBERG_ASSIGN_OR_RAISE(auto placeholder, SanitizePlaceholder(literal, now, today)); | |
| return Literal::String(std::move(placeholder)); | |
| } | |
| Result<std::shared_ptr<UnboundTerm<BoundTransform>>> MakeSanitizedTransformTerm( | |
| std::string_view name, const std::shared_ptr<Transform>& transform) { | |
| ICEBERG_ASSIGN_OR_RAISE(std::shared_ptr<NamedReference> named_ref, | |
| NamedReference::Make(std::string(name))); | |
| return UnboundTransform::Make(std::move(named_ref), transform); | |
| } | |
| template <typename B> | |
| Result<std::shared_ptr<Expression>> MakeSanitizedPredicateOverTerm( | |
| Expression::Operation op, std::shared_ptr<UnboundTerm<B>> term, | |
| std::vector<Literal> values) { | |
| if (values.empty()) { | |
| return UnboundPredicateImpl<B>::Make(op, term); | |
| } | |
| if (values.size() == 1) { | |
| return UnboundPredicateImpl<B>::Make(op, term, std::move(values[0])); | |
| } | |
| return UnboundPredicateImpl<B>::Make(op, term, std::move(values)); | |
| } | |
| // Rebuilds a sanitized predicate over a bound `term`, preserving whether it was a plain | |
| // column reference or a transform -- only the literal values are replaced with | |
| // placeholders. | |
| Result<std::shared_ptr<Expression>> MakeSanitizedPredicate( | |
| Expression::Operation op, const std::shared_ptr<BoundTerm>& term, | |
| std::vector<Literal> values) { | |
| if (term->kind() == Term::Kind::kTransform) { | |
| const auto& bound_transform = internal::checked_cast<const BoundTransform&>(*term); | |
| ICEBERG_ASSIGN_OR_RAISE(auto transform_term, | |
| MakeSanitizedTransformTerm(term->reference()->name(), | |
| bound_transform.transform())); | |
| return MakeSanitizedPredicateOverTerm<BoundTransform>(op, std::move(transform_term), | |
| std::move(values)); | |
| } | |
| ICEBERG_ASSIGN_OR_RAISE(auto named_ref, | |
| NamedReference::Make(std::string(term->reference()->name()))); | |
| return MakeSanitizedPredicateOverTerm<BoundReference>(op, std::move(named_ref), | |
| std::move(values)); | |
| } | |
| template <typename B> | |
| Result<std::shared_ptr<Expression>> MakeSanitizedUnboundPredicate( | |
| const std::shared_ptr<UnboundPredicate>& pred, std::vector<Literal> values) { | |
| auto typed_pred = std::dynamic_pointer_cast<UnboundPredicateImpl<B>>(pred); | |
| if (typed_pred == nullptr) [[unlikely]] { | |
| return InvalidExpression("Unexpected unbound predicate term type"); | |
| } | |
| return MakeSanitizedPredicateOverTerm<B>(pred->op(), typed_pred->term(), | |
| std::move(values)); | |
| } | |
| } // namespace | |
| SanitizeExpression::SanitizeExpression() { | |
| auto now = std::chrono::system_clock::now().time_since_epoch(); | |
| auto now_millis = std::chrono::duration_cast<std::chrono::milliseconds>(now); | |
| now_ = std::chrono::duration_cast<std::chrono::microseconds>(now_millis).count(); | |
| today_ = static_cast<int32_t>( | |
| std::chrono::duration_cast<std::chrono::days>(now_millis).count()); | |
| } | |
| Result<std::shared_ptr<Expression>> SanitizeExpression::Sanitize( | |
| const std::shared_ptr<Expression>& expr) { | |
| ICEBERG_DCHECK(expr != nullptr, "Expression cannot be null"); | |
| SanitizeExpression visitor; | |
| return Visit<std::shared_ptr<Expression>, SanitizeExpression>(expr, visitor); | |
| } | |
| Result<std::shared_ptr<Expression>> SanitizeExpression::AlwaysTrue() { | |
| return True::Instance(); | |
| } | |
| Result<std::shared_ptr<Expression>> SanitizeExpression::AlwaysFalse() { | |
| return False::Instance(); | |
| } | |
| Result<std::shared_ptr<Expression>> SanitizeExpression::Not( | |
| const std::shared_ptr<Expression>& child_result) { | |
| return Not::MakeFolded(child_result); | |
| } | |
| Result<std::shared_ptr<Expression>> SanitizeExpression::And( | |
| const std::shared_ptr<Expression>& left_result, | |
| const std::shared_ptr<Expression>& right_result) { | |
| return And::MakeFolded(left_result, right_result); | |
| } | |
| Result<std::shared_ptr<Expression>> SanitizeExpression::Or( | |
| const std::shared_ptr<Expression>& left_result, | |
| const std::shared_ptr<Expression>& right_result) { | |
| return Or::MakeFolded(left_result, right_result); | |
| } | |
| Result<std::shared_ptr<Expression>> SanitizeExpression::Predicate( | |
| const std::shared_ptr<BoundPredicate>& pred) { | |
| switch (pred->kind()) { | |
| case BoundPredicate::Kind::kUnary: | |
| return MakeSanitizedPredicate(pred->op(), pred->term(), {}); | |
| case BoundPredicate::Kind::kLiteral: { | |
| const auto& literal_pred = | |
| internal::checked_cast<const BoundLiteralPredicate&>(*pred); | |
| ICEBERG_ASSIGN_OR_RAISE(auto placeholder, | |
| SanitizeLiteral(literal_pred.literal(), now_, today_)); | |
| return MakeSanitizedPredicate(pred->op(), pred->term(), {std::move(placeholder)}); | |
| } | |
| case BoundPredicate::Kind::kSet: { | |
| const auto& set_pred = internal::checked_cast<const BoundSetPredicate&>(*pred); | |
| std::vector<Literal> placeholders; | |
| placeholders.reserve(set_pred.literal_set().size()); | |
| for (const auto& literal : set_pred.literal_set()) { | |
| ICEBERG_ASSIGN_OR_RAISE(auto placeholder, SanitizeLiteral(literal, now_, today_)); | |
| placeholders.push_back(std::move(placeholder)); | |
| } | |
| return MakeSanitizedPredicate(pred->op(), pred->term(), std::move(placeholders)); | |
| } | |
| } | |
| return InvalidExpression("Unsupported bound predicate kind for sanitization"); | |
| } | |
| Result<std::shared_ptr<Expression>> SanitizeExpression::Predicate( | |
| const std::shared_ptr<UnboundPredicate>& pred) { | |
| switch (pred->op()) { | |
| case Expression::Operation::kIsNull: | |
| case Expression::Operation::kNotNull: | |
| case Expression::Operation::kIsNan: | |
| case Expression::Operation::kNotNan: | |
| return pred; | |
| case Expression::Operation::kLt: | |
| case Expression::Operation::kLtEq: | |
| case Expression::Operation::kGt: | |
| case Expression::Operation::kGtEq: | |
| case Expression::Operation::kEq: | |
| case Expression::Operation::kNotEq: | |
| case Expression::Operation::kStartsWith: | |
| case Expression::Operation::kNotStartsWith: | |
| case Expression::Operation::kIn: | |
| case Expression::Operation::kNotIn: | |
| break; | |
| default: | |
| return InvalidExpression( | |
| "Unsupported unbound predicate operation for sanitization"); | |
| } | |
| auto literals = pred->literals(); | |
| std::vector<Literal> placeholders; | |
| placeholders.reserve(literals.size()); | |
| for (const auto& literal : literals) { | |
| ICEBERG_ASSIGN_OR_RAISE(auto placeholder, SanitizeLiteral(literal, now_, today_)); | |
| placeholders.push_back(std::move(placeholder)); | |
| } | |
| switch (pred->unbound_term().kind()) { | |
| case Term::Kind::kReference: | |
| return MakeSanitizedUnboundPredicate<BoundReference>(pred, std::move(placeholders)); | |
| case Term::Kind::kTransform: | |
| return MakeSanitizedUnboundPredicate<BoundTransform>(pred, std::move(placeholders)); | |
| case Term::Kind::kExtract: | |
| return NotSupported("Cannot sanitize an extract predicate"); | |
| } | |
| std::unreachable(); | |
| } | |
| Result<std::shared_ptr<Expression>> SanitizeExpression::Sanitize( | |
| const Schema& schema, const std::shared_ptr<Expression>& expr, bool case_sensitive) { | |
| auto bound = Binder::Bind(schema, expr, case_sensitive); | |
| if (bound.has_value()) { | |
| return Sanitize(*bound); | |
| } | |
| return Sanitize(expr); | |
| } | |
| // TODO(evindj) : add StringSanitizer for logging. | |
| } // namespace iceberg |