Files
scylladb/cql3/update_parameters.cc
Konstantin Osipov 670b2562a1 lwt: Cassandrda compatibility when incarnating a row for UPDATE
When evaluating an LWT condition involving both static and non-static
cells, and matching no regular row, the static row must be used UNLESS
the IF condition is IF EXISTS/IF NOT EXISTS, in which case special rules
apply.

Before this fix, Scylla used to assume a row doesn't exist if there is
no matching primary key. In Cassandra, if there is a
non-empty static row in the partition, a regular row based
on the static row' cell values is created in this case, and then this
row is used to evaluate the condition.

This problem was reported as gh-10081.

The reason for Scylla behaviour before the patch was that when
implementing LWT I tried to converge Cassandra data model (or lack of
thereof) with a relational data model, and assumed a static row is a
"shared" portion of a regular row, i.e. a storage level concept intended
to save space, and doesn't have independent existence.
This was an oversimplification.

This patch fixes gh-10081, making Scylla semantics match the one of
Cassandra.

I will now list other known examples when a static row has an own
independent existence as part of a table, for cataloguing purposes.

SELECT * from a partition which has a partition key
and a static cell set returns 1 row. If later a regular row is added
to the partition, the SELECT would still return 1 row, i.e.
the static row will disappear, and a regular row will appear instead.

Another example showing a static row has an independent existence below:

CREATE TABLE t (p int, c int, s int static, PRIMARY KEY(p, c));
INSERT INTO t (p, c) VALUES(1, 1);
INSERT INTO t (p, s) VALUES(1, 1) IF NOT EXISTS;

In Cassandra (and Scylla), IF NOT EXISTS evaluates to TRUE, even though both
the regular row and the partition exist. But the static cells are not
set, and the insert only provides a partition key, so the database assumes the
insert is operating against a static row.

It would be wrong to assume that a static row exists when the partition
key exists:
INSERT INTO t (p, c, s) VALUES(1, 1, 1) IF NOT EXISTS;

 [applied] | p | c | s
 -----------+---+---+------
      False | 1 | 1 | null

evaluates to False, i.e. the regular row does exist when p and c exist.

Issue

CREATE TABLE t (p INT, c INT, r INT, s INT static, PRIMARY KEY(p, c))
INSERT INTO t (p, s) VALUES (1, 1);
UPDATE t SET s=2, r=1 WHERE p=1 AND c=1 IF s=1 and r=null;
- in this case, even though the regular row doesn't exist, the static
row does, and should be used for condition evaluation.

In other words, IF EXISTS/IF NOT EXISTS have contextual semantics.
They apply to the regular row if clustering key is used in the WHERE
clause, otherwise they apply to static row.

One analogy for static rows is that it is like a static member of C++ or
Java class. It's an attribute of the class (assuming class = partition),
which is accessible through every object of the class (object = regular
row). It is also present if there are no objects of the class, but the
class itself exists: i.e. a partition could have no regular rows, but
some static cells set, in this case it has a static row.

*Unlike C++/Java static class members* a static row is an optional
attribute of the partition. A partition may exist, but the static row
may be absent (e.g. no static cell is set). If the static row does exist,
all regular rows share its contents, *even if they do not exist*.
A regular row exists when its clustering key is present
in the table. A static row exists when at least one static cell is set.

Tests are updated because now when no matching row is found
for the update we show the value of the static row as the previous
value, instead of a non-matching clustering row.

Changes in v2:
- reworded the commit message
- added select tests

Closes #10711
2022-06-16 19:23:46 +03:00

170 lines
5.9 KiB
C++

/*
* Copyright (C) 2015-present ScyllaDB
*
* Modified by ScyllaDB
*/
/*
* SPDX-License-Identifier: (AGPL-3.0-or-later and Apache-2.0)
*/
#include "cql3/update_parameters.hh"
#include "query-result-reader.hh"
#include "types/map.hh"
namespace cql3 {
const std::vector<std::pair<data_value, data_value>> *
update_parameters::get_prefetched_list(const partition_key& pkey, const clustering_key& ckey, const column_definition& column) const {
auto row = _prefetched.find_row(pkey, column.is_static() ? clustering_key::make_empty() : ckey);
if (row == nullptr) {
return nullptr;
}
auto j = row->cells.find(column.ordinal_id);
if (j == row->cells.end()) {
return nullptr;
}
const data_value& cell = j->second;
// Ensured by collections_as_maps flag in read_command flags
assert(cell.type()->is_map());
const map_type_impl& map_type = static_cast<const map_type_impl&>(*cell.type());
return &map_type.from_value(cell);
}
update_parameters::prefetch_data::prefetch_data(schema_ptr schema)
: rows(key_less{*schema})
, schema(schema)
{ }
const update_parameters::prefetch_data::row*
update_parameters::prefetch_data::find_row(const partition_key& pkey, const clustering_key& ckey) const {
const auto it = rows.find({pkey, ckey});
return it == rows.end() ? nullptr : &it->second;
}
// Implements ResultVisitor concept from query.hh
class prefetch_data_builder {
update_parameters::prefetch_data& _data;
const query::partition_slice& _ps;
schema_ptr _schema;
std::optional<partition_key> _pkey;
// Add partition key columns to the current full row
void add_partition_key(update_parameters::prefetch_data::row& cells, const partition_key& key)
{
auto i = key.begin(*_schema);
for (auto&& col : _schema->partition_key_columns()) {
cells.cells.emplace(col.ordinal_id, col.type->deserialize_value(*i));
++i;
}
}
// Add clustering key columns to the current full row
void add_clustering_key(update_parameters::prefetch_data::row& cells, const clustering_key& key)
{
auto i = key.begin(*_schema);
for (auto&& col : _schema->clustering_key_columns()) {
if (i == key.end(*_schema)) {
break;
}
cells.cells.emplace(col.ordinal_id, col.type->deserialize_value(*i));
++i;
}
}
// Add a prefetched cell to the current full row
void add_cell(update_parameters::prefetch_data::row& cells, const column_definition& def,
const std::optional<query::result_bytes_view>& cell) {
if (cell == std::nullopt) {
return;
}
auto type = def.type;
// We use collections_as_maps flag, so set/list type is map, reconstruct the
// data type used for serialization.
if (type->is_listlike() && type->is_multi_cell()) {
auto ctype = static_pointer_cast<const collection_type_impl>(type);
type = map_type_impl::get_instance(ctype->name_comparator(), ctype->value_comparator(), true);
}
cells.cells.emplace(def.ordinal_id, type->deserialize(*cell));
};
public:
prefetch_data_builder(schema_ptr s, update_parameters::prefetch_data& data, const query::partition_slice& ps)
: _data(data)
, _ps(ps)
, _schema(std::move(s))
{ }
void accept_new_partition(const partition_key& key, uint64_t row_count) {
_pkey = key;
}
void accept_new_partition(uint64_t row_count) {
assert(0);
}
void accept_new_row(const clustering_key& key, const query::result_row_view& static_row,
const query::result_row_view& row) {
update_parameters::prefetch_data::row cells;
add_partition_key(cells, *_pkey);
add_clustering_key(cells, key);
auto static_row_iterator = static_row.iterator();
for (auto&& id : _ps.static_columns) {
add_cell(cells, _schema->static_column_at(id), static_row_iterator.next_collection_cell());
}
auto row_iterator = row.iterator();
for (auto&& id : _ps.regular_columns) {
add_cell(cells, _schema->regular_column_at(id), row_iterator.next_collection_cell());
}
_data.rows.emplace(std::make_pair(*_pkey, key), std::move(cells));
}
void accept_new_row(const query::result_row_view& static_row, const query::result_row_view& row) {
assert(0);
}
void accept_partition_end(const query::result_row_view& static_row) {
if (!_schema->clustering_key_size() || !_schema->has_static_columns()) {
// Do not add an (empty) static row if there are no
// clustering key columns or not static cells, such
// row will have no non-null cells in it, so will
// be useless.
return;
}
// When no clustering row matches WHERE condition of
// UPSERT-like operation (INSERT, UPDATE)
// the static row will be used to materialize the initial
// clustering row.
update_parameters::prefetch_data::row cells;
add_partition_key(cells, *_pkey);
auto static_row_iterator = static_row.iterator();
for (auto&& id : _ps.static_columns) {
add_cell(cells, _schema->static_column_at(id), static_row_iterator.next_collection_cell());
}
// We end up here only if the table has a clustering key,
// so no other row added for this partition thus has an
// empty ckey.
_data.rows.emplace(std::make_pair(*_pkey, clustering_key_prefix::make_empty()), std::move(cells));
}
};
update_parameters::prefetch_data update_parameters::build_prefetch_data(schema_ptr schema, const query::result& query_result,
const query::partition_slice& slice) {
update_parameters::prefetch_data rows(schema);
query::result_view::consume(query_result, slice, prefetch_data_builder(schema, rows, slice));
return rows;
}
} // end of namespace cql3