mirror of
https://github.com/scylladb/scylladb.git
synced 2026-04-27 03:45:11 +00:00
before this change, we rely on `using namespace seastar` to use
`seastar::format()` without qualifying the `format()` with its
namespace. this works fine until we changed the parameter type
of format string `seastar::format()` from `const char*` to
`fmt::format_string<...>`. this change practically invited
`seastar::format()` to the club of `std::format()` and `fmt::format()`,
where all members accept a templated parameter as its `fmt`
parameter. and `seastar::format()` is not the best candidate anymore.
despite that argument-dependent lookup (ADT for short) favors the
function which is in the same namespace as its parameter, but
`using namespace` makes `seastar::format()` more competitive,
so both `std::format()` and `seastar::format()` are considered
as the condidates.
that is what is happening scylladb in quite a few caller sites of
`format()`, hence ADT is not able to tell which function the winner
in the name lookup:
```
/__w/scylladb/scylladb/mutation/mutation_fragment_stream_validator.cc:265:12: error: call to 'format' is ambiguous
265 | return format("{} ({}.{} {})", _name_view, s.ks_name(), s.cf_name(), s.id());
| ^~~~~~
/usr/bin/../lib/gcc/x86_64-redhat-linux/14/../../../../include/c++/14/format:4290:5: note: candidate function [with _Args = <const std::basic_string_view<char> &, const seastar::basic_sstring<char, unsigned int, 15> &, const seastar::basic_sstring<char, unsigned int, 15> &, const utils::tagged_uuid<table_id_tag> &>]
4290 | format(format_string<_Args...> __fmt, _Args&&... __args)
| ^
/__w/scylladb/scylladb/seastar/include/seastar/core/print.hh:143:1: note: candidate function [with A = <const std::basic_string_view<char> &, const seastar::basic_sstring<char, unsigned int, 15> &, const seastar::basic_sstring<char, unsigned int, 15> &, const utils::tagged_uuid<table_id_tag> &>]
143 | format(fmt::format_string<A...> fmt, A&&... a) {
| ^
```
in this change, we
change all `format()` to either `fmt::format()` or `seastar::format()`
with following rules:
- if the caller expects an `sstring` or `std::string_view`, change to
`seastar::format()`
- if the caller expects an `std::string`, change to `fmt::format()`.
because, `sstring::operator std::basic_string` would incur a deep
copy.
we will need another change to enable scylladb to compile with the
latest seastar. namely, to pass the format string as a templated
parameter down to helper functions which format their parameters.
to miminize the scope of this change, let's include that change when
bumping up the seastar submodule. as that change will depend on
the seastar change.
Signed-off-by: Kefu Chai <kefu.chai@scylladb.com>
759 lines
33 KiB
C++
759 lines
33 KiB
C++
/*
|
|
* Copyright 2019-present ScyllaDB
|
|
*/
|
|
|
|
/*
|
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
*/
|
|
|
|
#include <string_view>
|
|
#include "alternator/conditions.hh"
|
|
#include "alternator/error.hh"
|
|
#include <unordered_map>
|
|
#include "utils/rjson.hh"
|
|
#include "serialization.hh"
|
|
#include "utils/base64.hh"
|
|
#include "utils/rjson.hh"
|
|
#include <stdexcept>
|
|
#include <boost/algorithm/cxx11/all_of.hpp>
|
|
#include <boost/algorithm/cxx11/any_of.hpp>
|
|
#include "utils/overloaded_functor.hh"
|
|
|
|
#include "expressions.hh"
|
|
|
|
namespace alternator {
|
|
|
|
static logging::logger clogger("alternator-conditions");
|
|
|
|
comparison_operator_type get_comparison_operator(const rjson::value& comparison_operator) {
|
|
static std::unordered_map<std::string, comparison_operator_type> ops = {
|
|
{"EQ", comparison_operator_type::EQ},
|
|
{"NE", comparison_operator_type::NE},
|
|
{"LE", comparison_operator_type::LE},
|
|
{"LT", comparison_operator_type::LT},
|
|
{"GE", comparison_operator_type::GE},
|
|
{"GT", comparison_operator_type::GT},
|
|
{"IN", comparison_operator_type::IN},
|
|
{"NULL", comparison_operator_type::IS_NULL},
|
|
{"NOT_NULL", comparison_operator_type::NOT_NULL},
|
|
{"BETWEEN", comparison_operator_type::BETWEEN},
|
|
{"BEGINS_WITH", comparison_operator_type::BEGINS_WITH},
|
|
{"CONTAINS", comparison_operator_type::CONTAINS},
|
|
{"NOT_CONTAINS", comparison_operator_type::NOT_CONTAINS},
|
|
};
|
|
if (!comparison_operator.IsString()) {
|
|
throw api_error::validation(fmt::format("Invalid comparison operator definition {}", rjson::print(comparison_operator)));
|
|
}
|
|
std::string op = comparison_operator.GetString();
|
|
auto it = ops.find(op);
|
|
if (it == ops.end()) {
|
|
throw api_error::validation(fmt::format("Unsupported comparison operator {}", op));
|
|
}
|
|
return it->second;
|
|
}
|
|
|
|
namespace {
|
|
|
|
struct size_check {
|
|
// True iff size passes this check.
|
|
virtual bool operator()(rapidjson::SizeType size) const = 0;
|
|
// Check description, such that format("expected array {}", check.what()) is human-readable.
|
|
virtual sstring what() const = 0;
|
|
};
|
|
|
|
class exact_size : public size_check {
|
|
rapidjson::SizeType _expected;
|
|
public:
|
|
explicit exact_size(rapidjson::SizeType expected) : _expected(expected) {}
|
|
bool operator()(rapidjson::SizeType size) const override { return size == _expected; }
|
|
sstring what() const override { return format("of size {}", _expected); }
|
|
};
|
|
|
|
struct empty : public size_check {
|
|
bool operator()(rapidjson::SizeType size) const override { return size < 1; }
|
|
sstring what() const override { return "to be empty"; }
|
|
};
|
|
|
|
struct nonempty : public size_check {
|
|
bool operator()(rapidjson::SizeType size) const override { return size > 0; }
|
|
sstring what() const override { return "to be non-empty"; }
|
|
};
|
|
|
|
} // anonymous namespace
|
|
|
|
// Check that array has the expected number of elements
|
|
static void verify_operand_count(const rjson::value* array, const size_check& expected, const rjson::value& op) {
|
|
if (!array && expected(0)) {
|
|
// If expected() allows an empty AttributeValueList, it is also fine
|
|
// that it is missing.
|
|
return;
|
|
}
|
|
if (!array || !array->IsArray()) {
|
|
throw api_error::validation("With ComparisonOperator, AttributeValueList must be given and an array");
|
|
}
|
|
if (!expected(array->Size())) {
|
|
throw api_error::validation(
|
|
format("{} operator requires AttributeValueList {}, instead found list size {}",
|
|
op, expected.what(), array->Size()));
|
|
}
|
|
}
|
|
|
|
struct rjson_engaged_ptr_comp {
|
|
bool operator()(const rjson::value* p1, const rjson::value* p2) const {
|
|
return rjson::single_value_comp()(*p1, *p2);
|
|
}
|
|
};
|
|
|
|
// It's not enough to compare underlying JSON objects when comparing sets,
|
|
// as internally they're stored in an array, and the order of elements is
|
|
// not important in set equality. See issue #5021
|
|
static bool check_EQ_for_sets(const rjson::value& set1, const rjson::value& set2) {
|
|
if (!set1.IsArray() || !set2.IsArray() || set1.Size() != set2.Size()) {
|
|
return false;
|
|
}
|
|
std::set<const rjson::value*, rjson_engaged_ptr_comp> set1_raw;
|
|
for (auto it = set1.Begin(); it != set1.End(); ++it) {
|
|
set1_raw.insert(&*it);
|
|
}
|
|
for (const auto& a : set2.GetArray()) {
|
|
if (!set1_raw.contains(&a)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
// Moreover, the JSON being compared can be a nested document with outer
|
|
// layers of lists and maps and some inner set - and we need to get to that
|
|
// inner set to compare it correctly with check_EQ_for_sets() (issue #8514).
|
|
static bool check_EQ(const rjson::value* v1, const rjson::value& v2);
|
|
static bool check_EQ_for_lists(const rjson::value& list1, const rjson::value& list2) {
|
|
if (!list1.IsArray() || !list2.IsArray() || list1.Size() != list2.Size()) {
|
|
return false;
|
|
}
|
|
auto it1 = list1.Begin();
|
|
auto it2 = list2.Begin();
|
|
while (it1 != list1.End()) {
|
|
// Note: Alternator limits an item's depth (rjson::parse() limits
|
|
// it to around 37 levels), so this recursion is safe.
|
|
if (!check_EQ(&*it1, *it2)) {
|
|
return false;
|
|
}
|
|
++it1;
|
|
++it2;
|
|
}
|
|
return true;
|
|
}
|
|
static bool check_EQ_for_maps(const rjson::value& list1, const rjson::value& list2) {
|
|
if (!list1.IsObject() || !list2.IsObject() || list1.MemberCount() != list2.MemberCount()) {
|
|
return false;
|
|
}
|
|
for (auto it1 = list1.MemberBegin(); it1 != list1.MemberEnd(); ++it1) {
|
|
auto it2 = list2.FindMember(it1->name);
|
|
if (it2 == list2.MemberEnd() || !check_EQ(&it1->value, it2->value)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Check if two JSON-encoded values match with the EQ relation
|
|
static bool check_EQ(const rjson::value* v1, const rjson::value& v2) {
|
|
if (v1 && v1->IsObject() && v1->MemberCount() == 1 && v2.IsObject() && v2.MemberCount() == 1) {
|
|
auto it1 = v1->MemberBegin();
|
|
auto it2 = v2.MemberBegin();
|
|
if (it1->name != it2->name) {
|
|
return false;
|
|
}
|
|
if (it1->name == "SS" || it1->name == "NS" || it1->name == "BS") {
|
|
return check_EQ_for_sets(it1->value, it2->value);
|
|
} else if(it1->name == "L") {
|
|
return check_EQ_for_lists(it1->value, it2->value);
|
|
} else if(it1->name == "M") {
|
|
return check_EQ_for_maps(it1->value, it2->value);
|
|
} else {
|
|
// Other, non-nested types (number, string, etc.) can be compared
|
|
// literally, comparing their JSON representation.
|
|
return it1->value == it2->value;
|
|
}
|
|
} else {
|
|
// If v1 and/or v2 are missing (IsNull()) the result should be false.
|
|
// In the unlikely case that the object is malformed (issue #8070),
|
|
// let's also return false.
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Check if two JSON-encoded values match with the NE relation
|
|
static bool check_NE(const rjson::value* v1, const rjson::value& v2) {
|
|
return !check_EQ(v1, v2);
|
|
}
|
|
|
|
// Check if two JSON-encoded values match with the BEGINS_WITH relation
|
|
bool check_BEGINS_WITH(const rjson::value* v1, const rjson::value& v2,
|
|
bool v1_from_query, bool v2_from_query) {
|
|
bool bad = false;
|
|
if (!v1 || !v1->IsObject() || v1->MemberCount() != 1) {
|
|
if (v1_from_query) {
|
|
throw api_error::validation("begins_with() encountered malformed argument");
|
|
} else {
|
|
bad = true;
|
|
}
|
|
} else if (v1->MemberBegin()->name != "S" && v1->MemberBegin()->name != "B") {
|
|
if (v1_from_query) {
|
|
throw api_error::validation(format("begins_with supports only string or binary type, got: {}", *v1));
|
|
} else {
|
|
bad = true;
|
|
}
|
|
}
|
|
if (!v2.IsObject() || v2.MemberCount() != 1) {
|
|
if (v2_from_query) {
|
|
throw api_error::validation("begins_with() encountered malformed argument");
|
|
} else {
|
|
bad = true;
|
|
}
|
|
} else if (v2.MemberBegin()->name != "S" && v2.MemberBegin()->name != "B") {
|
|
if (v2_from_query) {
|
|
throw api_error::validation(format("begins_with() supports only string or binary type, got: {}", v2));
|
|
} else {
|
|
bad = true;
|
|
}
|
|
}
|
|
if (bad) {
|
|
return false;
|
|
}
|
|
auto it1 = v1->MemberBegin();
|
|
auto it2 = v2.MemberBegin();
|
|
if (it1->name != it2->name) {
|
|
return false;
|
|
}
|
|
if (it2->name == "S") {
|
|
return rjson::to_string_view(it1->value).starts_with(rjson::to_string_view(it2->value));
|
|
} else /* it2->name == "B" */ {
|
|
try {
|
|
return base64_begins_with(rjson::to_string_view(it1->value), rjson::to_string_view(it2->value));
|
|
} catch(std::invalid_argument&) {
|
|
// determine if any of the malformed values is from query and raise an exception if so
|
|
unwrap_bytes(it1->value, v1_from_query);
|
|
unwrap_bytes(it2->value, v2_from_query);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
static bool is_set_of(const rjson::value& type1, const rjson::value& type2) {
|
|
return (type2 == "S" && type1 == "SS") || (type2 == "N" && type1 == "NS") || (type2 == "B" && type1 == "BS");
|
|
}
|
|
|
|
// Check if two JSON-encoded values match with the CONTAINS relation
|
|
bool check_CONTAINS(const rjson::value* v1, const rjson::value& v2, bool v1_from_query, bool v2_from_query) {
|
|
if (!v1) {
|
|
return false;
|
|
}
|
|
const auto& kv1 = *v1->MemberBegin();
|
|
const auto& kv2 = *v2.MemberBegin();
|
|
if (kv1.name == "S" && kv2.name == "S") {
|
|
return rjson::to_string_view(kv1.value).find(rjson::to_string_view(kv2.value)) != std::string_view::npos;
|
|
} else if (kv1.name == "B" && kv2.name == "B") {
|
|
auto d_kv1 = unwrap_bytes(kv1.value, v1_from_query);
|
|
auto d_kv2 = unwrap_bytes(kv2.value, v2_from_query);
|
|
if (!d_kv1 || !d_kv2) {
|
|
return false;
|
|
}
|
|
return d_kv1->find(*d_kv2) != bytes::npos;
|
|
} else if (is_set_of(kv1.name, kv2.name)) {
|
|
for (auto i = kv1.value.Begin(); i != kv1.value.End(); ++i) {
|
|
if (*i == kv2.value) {
|
|
return true;
|
|
}
|
|
}
|
|
} else if (kv1.name == "L") {
|
|
for (auto i = kv1.value.Begin(); i != kv1.value.End(); ++i) {
|
|
if (!i->IsObject() || i->MemberCount() != 1) {
|
|
clogger.error("check_CONTAINS received a list whose element is malformed");
|
|
return false;
|
|
}
|
|
const auto& el = *i->MemberBegin();
|
|
if (el.name == kv2.name && el.value == kv2.value) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Check if two JSON-encoded values match with the NOT_CONTAINS relation
|
|
static bool check_NOT_CONTAINS(const rjson::value* v1, const rjson::value& v2, bool v1_from_query, bool v2_from_query) {
|
|
if (!v1) {
|
|
return false;
|
|
}
|
|
return !check_CONTAINS(v1, v2, v1_from_query, v2_from_query);
|
|
}
|
|
|
|
// Check if a JSON-encoded value equals any element of an array, which must have at least one element.
|
|
static bool check_IN(const rjson::value* val, const rjson::value& array) {
|
|
if (!array[0].IsObject() || array[0].MemberCount() != 1) {
|
|
throw api_error::validation(
|
|
format("IN operator encountered malformed AttributeValue: {}", array[0]));
|
|
}
|
|
const auto& type = array[0].MemberBegin()->name;
|
|
if (type != "S" && type != "N" && type != "B") {
|
|
throw api_error::validation(
|
|
"IN operator requires AttributeValueList elements to be of type String, Number, or Binary ");
|
|
}
|
|
if (!val) {
|
|
return false;
|
|
}
|
|
bool have_match = false;
|
|
for (const auto& elem : array.GetArray()) {
|
|
if (!elem.IsObject() || elem.MemberCount() != 1 || elem.MemberBegin()->name != type) {
|
|
throw api_error::validation(
|
|
"IN operator requires all AttributeValueList elements to have the same type ");
|
|
}
|
|
if (!have_match && *val == elem) {
|
|
// Can't return yet, must check types of all array elements. <sigh>
|
|
have_match = true;
|
|
}
|
|
}
|
|
return have_match;
|
|
}
|
|
|
|
// Another variant of check_IN, this one for ConditionExpression. It needs to
|
|
// check whether the first element in the given vector is equal to any of the
|
|
// others.
|
|
static bool check_IN(const std::vector<rjson::value>& array) {
|
|
const rjson::value* first = &array[0];
|
|
for (unsigned i = 1; i < array.size(); i++) {
|
|
if (check_EQ(first, array[i])) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool check_NULL(const rjson::value* val) {
|
|
return val == nullptr;
|
|
}
|
|
|
|
static bool check_NOT_NULL(const rjson::value* val) {
|
|
return val != nullptr;
|
|
}
|
|
|
|
// Only types S, N or B (string, number or bytes) may be compared by the
|
|
// various comparison operators - lt, le, gt, ge, and between.
|
|
// Note that in particular, if the value is missing (v->IsNull()), this
|
|
// check returns false.
|
|
static bool check_comparable_type(const rjson::value& v) {
|
|
if (!v.IsObject() || v.MemberCount() != 1) {
|
|
return false;
|
|
}
|
|
const rjson::value& type = v.MemberBegin()->name;
|
|
return type == "S" || type == "N" || type == "B";
|
|
}
|
|
|
|
// Check if two JSON-encoded values match with cmp.
|
|
template <typename Comparator>
|
|
bool check_compare(const rjson::value* v1, const rjson::value& v2, const Comparator& cmp,
|
|
bool v1_from_query, bool v2_from_query) {
|
|
bool bad = false;
|
|
if (!v1 || !check_comparable_type(*v1)) {
|
|
if (v1_from_query) {
|
|
throw api_error::validation(format("{} allow only the types String, Number, or Binary", cmp.diagnostic));
|
|
}
|
|
bad = true;
|
|
}
|
|
if (!check_comparable_type(v2)) {
|
|
if (v2_from_query) {
|
|
throw api_error::validation(format("{} allow only the types String, Number, or Binary", cmp.diagnostic));
|
|
}
|
|
bad = true;
|
|
}
|
|
if (bad) {
|
|
return false;
|
|
}
|
|
const auto& kv1 = *v1->MemberBegin();
|
|
const auto& kv2 = *v2.MemberBegin();
|
|
if (kv1.name != kv2.name) {
|
|
return false;
|
|
}
|
|
if (kv1.name == "N") {
|
|
return cmp(unwrap_number(*v1, cmp.diagnostic), unwrap_number(v2, cmp.diagnostic));
|
|
}
|
|
if (kv1.name == "S") {
|
|
return cmp(std::string_view(kv1.value.GetString(), kv1.value.GetStringLength()),
|
|
std::string_view(kv2.value.GetString(), kv2.value.GetStringLength()));
|
|
}
|
|
if (kv1.name == "B") {
|
|
auto d_kv1 = unwrap_bytes(kv1.value, v1_from_query);
|
|
auto d_kv2 = unwrap_bytes(kv2.value, v2_from_query);
|
|
if(!d_kv1 || !d_kv2) {
|
|
return false;
|
|
}
|
|
return cmp(*d_kv1, *d_kv2);
|
|
}
|
|
// cannot reach here, as check_comparable_type() verifies the type is one
|
|
// of the above options.
|
|
return false;
|
|
}
|
|
|
|
struct cmp_lt {
|
|
template <typename T> bool operator()(const T& lhs, const T& rhs) const { return lhs < rhs; }
|
|
// We cannot use the normal comparison operators like "<" on the bytes
|
|
// type, because they treat individual bytes as signed but we need to
|
|
// compare them as *unsigned*. So we need a specialization for bytes.
|
|
bool operator()(const bytes& lhs, const bytes& rhs) const { return compare_unsigned(lhs, rhs) < 0; }
|
|
static constexpr const char* diagnostic = "LT operator";
|
|
};
|
|
|
|
struct cmp_le {
|
|
template <typename T> bool operator()(const T& lhs, const T& rhs) const { return lhs <= rhs; }
|
|
bool operator()(const bytes& lhs, const bytes& rhs) const { return compare_unsigned(lhs, rhs) <= 0; }
|
|
static constexpr const char* diagnostic = "LE operator";
|
|
};
|
|
|
|
struct cmp_ge {
|
|
template <typename T> bool operator()(const T& lhs, const T& rhs) const { return lhs >= rhs; }
|
|
bool operator()(const bytes& lhs, const bytes& rhs) const { return compare_unsigned(lhs, rhs) >= 0; }
|
|
static constexpr const char* diagnostic = "GE operator";
|
|
};
|
|
|
|
struct cmp_gt {
|
|
template <typename T> bool operator()(const T& lhs, const T& rhs) const { return lhs > rhs; }
|
|
bool operator()(const bytes& lhs, const bytes& rhs) const { return compare_unsigned(lhs, rhs) > 0; }
|
|
static constexpr const char* diagnostic = "GT operator";
|
|
};
|
|
|
|
// True if v is between lb and ub, inclusive. Throws or returns false
|
|
// (depending on bounds_from_query parameter) if lb > ub.
|
|
template <typename T>
|
|
static bool check_BETWEEN(const T& v, const T& lb, const T& ub, bool bounds_from_query) {
|
|
if (cmp_lt()(ub, lb)) {
|
|
if (bounds_from_query) {
|
|
throw api_error::validation(
|
|
fmt::format("BETWEEN operator requires lower_bound <= upper_bound, but {} > {}", lb, ub));
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
return cmp_ge()(v, lb) && cmp_le()(v, ub);
|
|
}
|
|
|
|
static bool check_BETWEEN(const rjson::value* v, const rjson::value& lb, const rjson::value& ub,
|
|
bool v_from_query, bool lb_from_query, bool ub_from_query) {
|
|
if ((v && v_from_query && !check_comparable_type(*v)) ||
|
|
(lb_from_query && !check_comparable_type(lb)) ||
|
|
(ub_from_query && !check_comparable_type(ub))) {
|
|
throw api_error::validation("between allow only the types String, Number, or Binary");
|
|
|
|
}
|
|
if (!v || !v->IsObject() || v->MemberCount() != 1 ||
|
|
!lb.IsObject() || lb.MemberCount() != 1 ||
|
|
!ub.IsObject() || ub.MemberCount() != 1) {
|
|
return false;
|
|
}
|
|
|
|
const auto& kv_v = *v->MemberBegin();
|
|
const auto& kv_lb = *lb.MemberBegin();
|
|
const auto& kv_ub = *ub.MemberBegin();
|
|
bool bounds_from_query = lb_from_query && ub_from_query;
|
|
if (kv_lb.name != kv_ub.name) {
|
|
if (bounds_from_query) {
|
|
throw api_error::validation(
|
|
format("BETWEEN operator requires the same type for lower and upper bound; instead got {} and {}",
|
|
kv_lb.name, kv_ub.name));
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
if (kv_v.name != kv_lb.name) { // Cannot compare different types, so v is NOT between lb and ub.
|
|
return false;
|
|
}
|
|
if (kv_v.name == "N") {
|
|
const char* diag = "BETWEEN operator";
|
|
return check_BETWEEN(unwrap_number(*v, diag), unwrap_number(lb, diag), unwrap_number(ub, diag), bounds_from_query);
|
|
}
|
|
if (kv_v.name == "S") {
|
|
return check_BETWEEN(std::string_view(kv_v.value.GetString(), kv_v.value.GetStringLength()),
|
|
std::string_view(kv_lb.value.GetString(), kv_lb.value.GetStringLength()),
|
|
std::string_view(kv_ub.value.GetString(), kv_ub.value.GetStringLength()),
|
|
bounds_from_query);
|
|
}
|
|
if (kv_v.name == "B") {
|
|
auto d_kv_v = unwrap_bytes(kv_v.value, v_from_query);
|
|
auto d_kv_lb = unwrap_bytes(kv_lb.value, lb_from_query);
|
|
auto d_kv_ub = unwrap_bytes(kv_ub.value, ub_from_query);
|
|
if(!d_kv_v || !d_kv_lb || !d_kv_ub) {
|
|
return false;
|
|
}
|
|
return check_BETWEEN(*d_kv_v, *d_kv_lb, *d_kv_ub, bounds_from_query);
|
|
}
|
|
if (v_from_query) {
|
|
throw api_error::validation(
|
|
format("BETWEEN operator requires AttributeValueList elements to be of type String, Number, or Binary; instead got {}",
|
|
kv_lb.name));
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Verify one Expect condition on one attribute (whose content is "got")
|
|
// for the verify_expected() below.
|
|
// This function returns true or false depending on whether the condition
|
|
// succeeded - it does not throw ConditionalCheckFailedException.
|
|
// However, it may throw ValidationException on input validation errors.
|
|
static bool verify_expected_one(const rjson::value& condition, const rjson::value* got) {
|
|
const rjson::value* comparison_operator = rjson::find(condition, "ComparisonOperator");
|
|
const rjson::value* attribute_value_list = rjson::find(condition, "AttributeValueList");
|
|
const rjson::value* value = rjson::find(condition, "Value");
|
|
const rjson::value* exists = rjson::find(condition, "Exists");
|
|
// There are three types of conditions that Expected supports:
|
|
// A value, not-exists, and a comparison of some kind. Each allows
|
|
// and requires a different combinations of parameters in the request
|
|
if (value) {
|
|
if (exists && (!exists->IsBool() || exists->GetBool() != true)) {
|
|
throw api_error::validation("Cannot combine Value with Exists!=true");
|
|
}
|
|
if (comparison_operator) {
|
|
throw api_error::validation("Cannot combine Value with ComparisonOperator");
|
|
}
|
|
return check_EQ(got, *value);
|
|
} else if (exists) {
|
|
if (comparison_operator) {
|
|
throw api_error::validation("Cannot combine Exists with ComparisonOperator");
|
|
}
|
|
if (!exists->IsBool() || exists->GetBool() != false) {
|
|
throw api_error::validation("Exists!=false requires Value");
|
|
}
|
|
// Remember Exists=false, so we're checking that the attribute does *not* exist:
|
|
return !got;
|
|
} else {
|
|
if (!comparison_operator) {
|
|
throw api_error::validation("Missing ComparisonOperator, Value or Exists");
|
|
}
|
|
comparison_operator_type op = get_comparison_operator(*comparison_operator);
|
|
switch (op) {
|
|
case comparison_operator_type::EQ:
|
|
verify_operand_count(attribute_value_list, exact_size(1), *comparison_operator);
|
|
return check_EQ(got, (*attribute_value_list)[0]);
|
|
case comparison_operator_type::NE:
|
|
verify_operand_count(attribute_value_list, exact_size(1), *comparison_operator);
|
|
return check_NE(got, (*attribute_value_list)[0]);
|
|
case comparison_operator_type::LT:
|
|
verify_operand_count(attribute_value_list, exact_size(1), *comparison_operator);
|
|
return check_compare(got, (*attribute_value_list)[0], cmp_lt{}, false, true);
|
|
case comparison_operator_type::LE:
|
|
verify_operand_count(attribute_value_list, exact_size(1), *comparison_operator);
|
|
return check_compare(got, (*attribute_value_list)[0], cmp_le{}, false, true);
|
|
case comparison_operator_type::GT:
|
|
verify_operand_count(attribute_value_list, exact_size(1), *comparison_operator);
|
|
return check_compare(got, (*attribute_value_list)[0], cmp_gt{}, false, true);
|
|
case comparison_operator_type::GE:
|
|
verify_operand_count(attribute_value_list, exact_size(1), *comparison_operator);
|
|
return check_compare(got, (*attribute_value_list)[0], cmp_ge{}, false, true);
|
|
case comparison_operator_type::BEGINS_WITH:
|
|
verify_operand_count(attribute_value_list, exact_size(1), *comparison_operator);
|
|
return check_BEGINS_WITH(got, (*attribute_value_list)[0], false, true);
|
|
case comparison_operator_type::IN:
|
|
verify_operand_count(attribute_value_list, nonempty(), *comparison_operator);
|
|
return check_IN(got, *attribute_value_list);
|
|
case comparison_operator_type::IS_NULL:
|
|
verify_operand_count(attribute_value_list, empty(), *comparison_operator);
|
|
return check_NULL(got);
|
|
case comparison_operator_type::NOT_NULL:
|
|
verify_operand_count(attribute_value_list, empty(), *comparison_operator);
|
|
return check_NOT_NULL(got);
|
|
case comparison_operator_type::BETWEEN:
|
|
verify_operand_count(attribute_value_list, exact_size(2), *comparison_operator);
|
|
return check_BETWEEN(got, (*attribute_value_list)[0], (*attribute_value_list)[1],
|
|
false, true, true);
|
|
case comparison_operator_type::CONTAINS:
|
|
{
|
|
verify_operand_count(attribute_value_list, exact_size(1), *comparison_operator);
|
|
// Expected's "CONTAINS" has this artificial limitation.
|
|
// ConditionExpression's "contains()" does not...
|
|
const rjson::value& arg = (*attribute_value_list)[0];
|
|
const auto& argtype = (*arg.MemberBegin()).name;
|
|
if (argtype != "S" && argtype != "N" && argtype != "B") {
|
|
throw api_error::validation(
|
|
format("CONTAINS operator requires a single AttributeValue of type String, Number, or Binary, "
|
|
"got {} instead", argtype));
|
|
}
|
|
return check_CONTAINS(got, arg, false, true);
|
|
}
|
|
case comparison_operator_type::NOT_CONTAINS:
|
|
{
|
|
verify_operand_count(attribute_value_list, exact_size(1), *comparison_operator);
|
|
// Expected's "NOT_CONTAINS" has this artificial limitation.
|
|
// ConditionExpression's "contains()" does not...
|
|
const rjson::value& arg = (*attribute_value_list)[0];
|
|
const auto& argtype = (*arg.MemberBegin()).name;
|
|
if (argtype != "S" && argtype != "N" && argtype != "B") {
|
|
throw api_error::validation(
|
|
format("CONTAINS operator requires a single AttributeValue of type String, Number, or Binary, "
|
|
"got {} instead", argtype));
|
|
}
|
|
return check_NOT_CONTAINS(got, arg, false, true);
|
|
}
|
|
}
|
|
throw std::logic_error(format("Internal error: corrupted operator enum: {}", int(op)));
|
|
}
|
|
}
|
|
|
|
conditional_operator_type get_conditional_operator(const rjson::value& req) {
|
|
const rjson::value* conditional_operator = rjson::find(req, "ConditionalOperator");
|
|
if (!conditional_operator) {
|
|
return conditional_operator_type::MISSING;
|
|
}
|
|
if (!conditional_operator->IsString()) {
|
|
throw api_error::validation("'ConditionalOperator' parameter, if given, must be a string");
|
|
}
|
|
auto s = rjson::to_string_view(*conditional_operator);
|
|
if (s == "AND") {
|
|
return conditional_operator_type::AND;
|
|
} else if (s == "OR") {
|
|
return conditional_operator_type::OR;
|
|
} else {
|
|
throw api_error::validation(
|
|
fmt::format("'ConditionalOperator' parameter must be AND, OR or missing. Found {}.", s));
|
|
}
|
|
}
|
|
|
|
// Check if the existing values of the item (previous_item) match the
|
|
// conditions given by the Expected and ConditionalOperator parameters
|
|
// (if they exist) in the request (an UpdateItem, PutItem or DeleteItem).
|
|
// This function can throw an ValidationException API error if there
|
|
// are errors in the format of the condition itself.
|
|
bool verify_expected(const rjson::value& req, const rjson::value* previous_item) {
|
|
const rjson::value* expected = rjson::find(req, "Expected");
|
|
auto conditional_operator = get_conditional_operator(req);
|
|
if (conditional_operator != conditional_operator_type::MISSING &&
|
|
(!expected || (expected->IsObject() && expected->GetObject().ObjectEmpty()))) {
|
|
throw api_error::validation("'ConditionalOperator' parameter cannot be specified for missing or empty Expression");
|
|
}
|
|
if (!expected) {
|
|
return true;
|
|
}
|
|
if (!expected->IsObject()) {
|
|
throw api_error::validation("'Expected' parameter, if given, must be an object");
|
|
}
|
|
bool require_all = conditional_operator != conditional_operator_type::OR;
|
|
return verify_condition(*expected, require_all, previous_item);
|
|
}
|
|
|
|
bool verify_condition(const rjson::value& condition, bool require_all, const rjson::value* previous_item) {
|
|
for (auto it = condition.MemberBegin(); it != condition.MemberEnd(); ++it) {
|
|
const rjson::value* got = nullptr;
|
|
if (previous_item) {
|
|
got = rjson::find(*previous_item, rjson::to_string_view(it->name));
|
|
}
|
|
bool success = verify_expected_one(it->value, got);
|
|
if (success && !require_all) {
|
|
// When !require_all, one success is enough!
|
|
return true;
|
|
} else if (!success && require_all) {
|
|
// When require_all, one failure is enough!
|
|
return false;
|
|
}
|
|
}
|
|
// If we got here and require_all, none of the checks failed, so succeed.
|
|
// If we got here and !require_all, all of the checks failed, so fail.
|
|
return require_all;
|
|
}
|
|
|
|
static bool calculate_primitive_condition(const parsed::primitive_condition& cond,
|
|
const rjson::value* previous_item) {
|
|
std::vector<rjson::value> calculated_values;
|
|
calculated_values.reserve(cond._values.size());
|
|
for (const parsed::value& v : cond._values) {
|
|
calculated_values.push_back(calculate_value(v,
|
|
cond._op == parsed::primitive_condition::type::VALUE ?
|
|
calculate_value_caller::ConditionExpressionAlone :
|
|
calculate_value_caller::ConditionExpression,
|
|
previous_item));
|
|
}
|
|
switch (cond._op) {
|
|
case parsed::primitive_condition::type::BETWEEN:
|
|
if (calculated_values.size() != 3) {
|
|
// Shouldn't happen unless we have a bug in the parser
|
|
throw std::logic_error(format("Wrong number of values {} in BETWEEN primitive_condition", cond._values.size()));
|
|
}
|
|
return check_BETWEEN(&calculated_values[0], calculated_values[1], calculated_values[2],
|
|
cond._values[0].is_constant(), cond._values[1].is_constant(), cond._values[2].is_constant());
|
|
case parsed::primitive_condition::type::IN:
|
|
return check_IN(calculated_values);
|
|
case parsed::primitive_condition::type::VALUE:
|
|
if (calculated_values.size() != 1) {
|
|
// Shouldn't happen unless we have a bug in the parser
|
|
throw std::logic_error(format("Unexpected values in primitive_condition", cond._values.size()));
|
|
}
|
|
// Unwrap the boolean wrapped as the value (if it is a boolean)
|
|
if (calculated_values[0].IsObject() && calculated_values[0].MemberCount() == 1) {
|
|
auto it = calculated_values[0].MemberBegin();
|
|
if (it->name == "BOOL" && it->value.IsBool()) {
|
|
return it->value.GetBool();
|
|
}
|
|
}
|
|
throw api_error::validation(
|
|
format("ConditionExpression: condition results in a non-boolean value: {}",
|
|
calculated_values[0]));
|
|
default:
|
|
// All the rest of the operators have exactly two parameters (and unless
|
|
// we have a bug in the parser, that's what we have in the parsed object:
|
|
if (calculated_values.size() != 2) {
|
|
throw std::logic_error(format("Wrong number of values {} in primitive_condition object", cond._values.size()));
|
|
}
|
|
}
|
|
switch (cond._op) {
|
|
case parsed::primitive_condition::type::EQ:
|
|
return check_EQ(&calculated_values[0], calculated_values[1]);
|
|
case parsed::primitive_condition::type::NE:
|
|
return check_NE(&calculated_values[0], calculated_values[1]);
|
|
case parsed::primitive_condition::type::GT:
|
|
return check_compare(&calculated_values[0], calculated_values[1], cmp_gt{},
|
|
cond._values[0].is_constant(), cond._values[1].is_constant());
|
|
case parsed::primitive_condition::type::GE:
|
|
return check_compare(&calculated_values[0], calculated_values[1], cmp_ge{},
|
|
cond._values[0].is_constant(), cond._values[1].is_constant());
|
|
case parsed::primitive_condition::type::LT:
|
|
return check_compare(&calculated_values[0], calculated_values[1], cmp_lt{},
|
|
cond._values[0].is_constant(), cond._values[1].is_constant());
|
|
case parsed::primitive_condition::type::LE:
|
|
return check_compare(&calculated_values[0], calculated_values[1], cmp_le{},
|
|
cond._values[0].is_constant(), cond._values[1].is_constant());
|
|
default:
|
|
// Shouldn't happen unless we have a bug in the parser
|
|
throw std::logic_error(format("Unknown type {} in primitive_condition object", (int)(cond._op)));
|
|
}
|
|
}
|
|
|
|
// Check if the existing values of the item (previous_item) match the
|
|
// conditions given by the given parsed ConditionExpression.
|
|
bool verify_condition_expression(
|
|
const parsed::condition_expression& condition_expression,
|
|
const rjson::value* previous_item) {
|
|
if (condition_expression.empty()) {
|
|
return true;
|
|
}
|
|
bool ret = std::visit(overloaded_functor {
|
|
[&] (const parsed::primitive_condition& cond) -> bool {
|
|
return calculate_primitive_condition(cond, previous_item);
|
|
},
|
|
[&] (const parsed::condition_expression::condition_list& list) -> bool {
|
|
auto verify_condition = [&] (const parsed::condition_expression& e) {
|
|
return verify_condition_expression(e, previous_item);
|
|
};
|
|
switch (list.op) {
|
|
case '&':
|
|
return boost::algorithm::all_of(list.conditions, verify_condition);
|
|
case '|':
|
|
return boost::algorithm::any_of(list.conditions, verify_condition);
|
|
default:
|
|
// Shouldn't happen unless we have a bug in the parser
|
|
throw std::logic_error("bad operator in condition_list");
|
|
}
|
|
}
|
|
}, condition_expression._expression);
|
|
return condition_expression._negated ? !ret : ret;
|
|
}
|
|
|
|
}
|