blob: 1fc5694ed222617d3dcbe1727fdb0e6169aa8b7f [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 "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