The CQL binary protocol introduced "unset" values in version 4
of the protocol. Unset values can be bound to variables, which
cause certain CQL fragments to be skipped. For example, the
fragment `SET a = :var` will not change the value of `a` if `:var`
is bound to an unset value.
Unsets, however, are very limited in where they can appear. They
can only appear at the top-level of an expression, and any computation
done with them is invalid. For example, `SET list_column = [3, :var]`
is invalid if `:var` is bound to unset.
This causes the code to be littered with checks for unset, and there
are plenty of tests dedicated to catching unsets. However, a simpler
way is possible - prevent the infiltration of unsets at the point of
entry (when evaluating a bind variable expression), and introduce
guards to check for the few cases where unsets are allowed.
This is what this long patch does. It performs the following:
(general)
1. unset is removed from the possible values of cql3::raw_value and
cql3::raw_value_view.
(external->cql3)
2. query_options is fortified with a vector of booleans,
unset_bind_variable_vector, where each boolean corresponds to a bind
variable index and is true when it is unset.
3. To avoid churn, two compatiblity structs are introduced:
cql3::raw_value{,_view}_vector_with_unset, which can be constructed
from a std::vector<raw_value{,_view/}>, which is what most callers
have. They can also be constructed with explicit unset vectors, for
the few cases they are needed.
(cql3->variables)
4. query_options::get_value_at() now throws if the requested bind variable
is unset. This replaces all the throwing checks in expression evaluation
and statement execution, which are removed.
5. A new query_options::is_unset() is added for the users that can tolerate
unset; though it is not used directly.
6. A new cql3::unset_operation_guard class guards against unsets. It accepts
an expression, and can be queried whether an unset is present. Two
conditions are checked: the expression must be a singleton bind
variable, and at runtime it must be bound to an unset value.
7. The modification_statement operations are split into two, via two
new subclasses of cql3::operation. cql3::operation_no_unset_support
ignores unsets completely. cql3::operation_skip_if_unset checks if
an operand is unset (luckily all operations have at most one operand that
tolerates unset) and applies unset_operation_guard to it.
8. The various sites that accept expressions or operations are modified
to check for should_skip_operation(). This are the loops around
operations in update_statement and delete_statement, and the checks
for unset in attributes (LIMIT and PER PARTITION LIMIT)
(tests)
9. Many unset tests are removed. It's now impossible to enter an
unset value into the expression evaluation machinery (there's
just no unset value), so it's impossible to test for it.
10. Other unset tests now have to be invoked via bind variables,
since there's no way to create an unset cql3::expr::constant.
11. Many tests have their exception message match strings relaxed.
Since unsets are now checked very early, we don't know the context
where they happen. It would be possible to reintroduce it (by adding
a format string parameter to cql3::unset_operation_guard), but it
seems not to be worth the effort. Usage of unsets is rare, and it is
explicit (at least with the Python driver, an unset cannot be
introduced by ommission).
I tried as an alternative to wrap cql3::raw_value{,_view} (that doesn't
recognize unsets) with cql3::maybe_unset_value (that does), but that
caused huge amounts of churn, so I abandoned that in favor of the
current approach.
Closes #12517
111 lines
4.0 KiB
C++
111 lines
4.0 KiB
C++
/*
|
|
* Copyright (C) 2015-present ScyllaDB
|
|
*
|
|
* Modified by ScyllaDB
|
|
*/
|
|
|
|
/*
|
|
* SPDX-License-Identifier: (AGPL-3.0-or-later and Apache-2.0)
|
|
*/
|
|
|
|
#include "maps.hh"
|
|
#include "cql3/abstract_marker.hh"
|
|
#include "operation.hh"
|
|
#include "update_parameters.hh"
|
|
#include "exceptions/exceptions.hh"
|
|
#include "cql3/cql3_type.hh"
|
|
#include "constants.hh"
|
|
#include "types/map.hh"
|
|
|
|
namespace cql3 {
|
|
void
|
|
maps::setter::execute(mutation& m, const clustering_key_prefix& row_key, const update_parameters& params) {
|
|
cql3::raw_value value = expr::evaluate(*_e, params._options);
|
|
execute(m, row_key, params, column, value);
|
|
}
|
|
|
|
void
|
|
maps::setter::execute(mutation& m, const clustering_key_prefix& row_key, const update_parameters& params, const column_definition& column, const cql3::raw_value& value) {
|
|
if (column.type->is_multi_cell()) {
|
|
// Delete all cells first, then put new ones
|
|
collection_mutation_description mut;
|
|
mut.tomb = params.make_tombstone_just_before();
|
|
m.set_cell(row_key, column, mut.serialize(*column.type));
|
|
}
|
|
do_put(m, row_key, params, value, column);
|
|
}
|
|
|
|
void
|
|
maps::setter_by_key::fill_prepare_context(prepare_context& ctx) {
|
|
operation::fill_prepare_context(ctx);
|
|
expr::fill_prepare_context(_k, ctx);
|
|
}
|
|
|
|
void
|
|
maps::setter_by_key::execute(mutation& m, const clustering_key_prefix& prefix, const update_parameters& params) {
|
|
using exceptions::invalid_request_exception;
|
|
assert(column.type->is_multi_cell()); // "Attempted to set a value for a single key on a frozen map"m
|
|
auto key = expr::evaluate(_k, params._options);
|
|
auto value = expr::evaluate(*_e, params._options);
|
|
if (key.is_null()) {
|
|
throw invalid_request_exception("Invalid null map key");
|
|
}
|
|
auto ctype = static_cast<const map_type_impl*>(column.type.get());
|
|
auto avalue = !value.is_null() ?
|
|
params.make_cell(*ctype->get_values_type(), value.view(), atomic_cell::collection_member::yes)
|
|
: params.make_dead_cell();
|
|
collection_mutation_description update;
|
|
update.cells.emplace_back(std::move(key).to_bytes(), std::move(avalue));
|
|
|
|
m.set_cell(prefix, column, update.serialize(*ctype));
|
|
}
|
|
|
|
void
|
|
maps::putter::execute(mutation& m, const clustering_key_prefix& prefix, const update_parameters& params) {
|
|
assert(column.type->is_multi_cell()); // "Attempted to add items to a frozen map";
|
|
cql3::raw_value value = expr::evaluate(*_e, params._options);
|
|
do_put(m, prefix, params, value, column);
|
|
}
|
|
|
|
void
|
|
maps::do_put(mutation& m, const clustering_key_prefix& prefix, const update_parameters& params,
|
|
const cql3::raw_value& map_value, const column_definition& column) {
|
|
if (column.type->is_multi_cell()) {
|
|
if (map_value.is_null()) {
|
|
return;
|
|
}
|
|
|
|
collection_mutation_description mut;
|
|
|
|
auto ctype = static_cast<const map_type_impl*>(column.type.get());
|
|
for (auto&& e : expr::get_map_elements(map_value)) {
|
|
mut.cells.emplace_back(to_bytes(e.first), params.make_cell(*ctype->get_values_type(), raw_value_view::make_value(e.second), atomic_cell::collection_member::yes));
|
|
}
|
|
|
|
m.set_cell(prefix, column, mut.serialize(*ctype));
|
|
} else {
|
|
// for frozen maps, we're overwriting the whole cell
|
|
if (map_value.is_null()) {
|
|
m.set_cell(prefix, column, params.make_dead_cell());
|
|
} else {
|
|
m.set_cell(prefix, column, params.make_cell(*column.type, map_value.view()));
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
maps::discarder_by_key::execute(mutation& m, const clustering_key_prefix& prefix, const update_parameters& params) {
|
|
assert(column.type->is_multi_cell()); // "Attempted to delete a single key in a frozen map";
|
|
cql3::raw_value key = expr::evaluate(*_e, params._options);
|
|
if (key.is_null()) {
|
|
throw exceptions::invalid_request_exception("Invalid null map key");
|
|
}
|
|
collection_mutation_description mut;
|
|
mut.cells.emplace_back(std::move(key).to_bytes(), params.make_dead_cell());
|
|
|
|
m.set_cell(prefix, column, mut.serialize(*column.type));
|
|
}
|
|
|
|
}
|
|
|