cql3: prepare and evaluate WRITETIME/TTL on collection elements and UDT fields

Complete the implementation of SELECT WRITETIME(col[key])/TTL(col[key])
and WRITETIME(col.field)/TTL(col.field), building on the grammar (commit 1),
wire format (commit 2), and selection-layer (commit 3) changes in the
preceding patches.

* prepare_column_mutation_attribute() (prepare_expr.cc) now handles the
  subscript and field_selection nodes that the grammar produces:
  - For subscripts, it validates that the inner column is a non-frozen
    map or set and checks the 'writetime_ttl_individual_element' feature
    flag so the feature is rejected during rolling upgrades.
  - For field selections, it validates that the inner column is a
    non-frozen UDT, with the same feature-flag check.

* do_evaluate(column_mutation_attribute) (expression.cc) handles the
  same two cases. For a field selection it serializes the field index as
  a key and looks it up in collection_element_metadata; for a subscript
  it evaluates the subscript key and looks it up in the same map.
  A missing key (element not found or expired) returns NULL, matching
  Cassandra behavior.

Together with the preceding three patches, this finally fixes #15427.

The next three patches will add tests and documentation for the new
feature, and the final eighth patch will fix the implementation of
UDT fields in LWT expressions - which the first patch made the grammar
allow but is still not implemented correctly.
This commit is contained in:
Nadav Har'El
2026-04-12 11:06:59 +03:00
parent 4ac63de063
commit 35e807a36c
2 changed files with 86 additions and 0 deletions

View File

@@ -1206,6 +1206,58 @@ cql3::raw_value do_evaluate(const field_selection& field_select, const evaluatio
static
cql3::raw_value
do_evaluate(const column_mutation_attribute& cma, const evaluation_inputs& inputs) {
// Helper for WRITETIME/TTL on a collection element or UDT field: given the
// inner column and the serialized element key, validate the index and look
// up the per-element timestamp or TTL in collection_element_metadata.
auto lookup_element_attribute = [&](const column_value* inner_col, std::string_view context, bytes key) -> cql3::raw_value {
int32_t index = inputs.selection->index_of(*inner_col->col);
if (inputs.collection_element_metadata.empty() || index < 0 || size_t(index) >= inputs.collection_element_metadata.size()) {
on_internal_error(expr_logger, fmt::format("evaluating column_mutation_attribute {}: column {} is not in selection",
context, inner_col->col->name_as_text()));
}
const auto& meta = inputs.collection_element_metadata[index];
switch (cma.kind) {
case column_mutation_attribute::attribute_kind::writetime: {
const auto it = meta.timestamps.find(key);
if (it == meta.timestamps.end()) {
return cql3::raw_value::make_null();
}
return raw_value::make_value(data_value(it->second).serialize());
}
case column_mutation_attribute::attribute_kind::ttl: {
const auto it = meta.ttls.find(key);
// The test it->second <= 0 (rather than < 0) matches the
// single-TTL check ttl_v <= 0 below.
if (it == meta.ttls.end() || it->second <= 0) {
return cql3::raw_value::make_null();
}
return raw_value::make_value(data_value(it->second).serialize());
}
}
on_internal_error(expr_logger, fmt::format("evaluating column_mutation_attribute {} with unexpected kind", context));
};
// Handle WRITETIME(x.field) / TTL(x.field) on a UDT field
if (auto fs = expr::as_if<field_selection>(&cma.column)) {
auto inner_col = expr::as_if<column_value>(&fs->structure);
if (!inner_col) {
on_internal_error(expr_logger, fmt::format("evaluating column_mutation_attribute field_selection: inner expression is not a column: {}", fs->structure));
}
return lookup_element_attribute(inner_col, "field_selection", serialize_field_index(fs->field_idx));
}
// Handle WRITETIME(m[key]) / TTL(m[key]) on a map element
if (auto sub = expr::as_if<subscript>(&cma.column)) {
auto inner_col = expr::as_if<column_value>(&sub->val);
if (!inner_col) {
on_internal_error(expr_logger, fmt::format("evaluating column_mutation_attribute subscript: inner expression is not a column: {}", sub->val));
}
auto evaluated_key = evaluate(sub->sub, inputs);
if (evaluated_key.is_null()) {
return cql3::raw_value::make_null();
}
return evaluated_key.view().with_linearized([&] (bytes_view key_bv) {
return lookup_element_attribute(inner_col, "subscript", bytes(key_bv));
});
}
auto col = expr::as_if<column_value>(&cma.column);
if (!col) {
on_internal_error(expr_logger, fmt::format("evaluating column_mutation_attribute of non-column {}", cma.column));

View File

@@ -1259,6 +1259,40 @@ prepare_column_mutation_attribute(
receiver->type->name(), receiver->name->text()));
}
auto column = prepare_expression(cma.column, db, keyspace, schema_opt, nullptr);
// Helper for the subscript and field-selection cases below: validates that
// inner_expr is a column, not a primary key column, that its type satisfies
// type_allowed, and that the cluster feature flag is on.
auto validate_and_return =
[&](const expression& inner_expr, std::string_view context,
auto type_allowed, std::string_view type_allowed_str) -> std::optional<expression> {
auto inner_cval = expr::as_if<column_value>(&inner_expr);
if (!inner_cval) {
throw exceptions::invalid_request_exception(fmt::format("{} on a {} expects a column, got {}", cma.kind, context, inner_expr));
}
if (inner_cval->col->is_primary_key()) {
throw exceptions::invalid_request_exception(fmt::format("{} is not legal on primary key component {}", cma.kind, inner_cval->col->name_as_text()));
}
if (!type_allowed(inner_cval->col->type)) {
throw exceptions::invalid_request_exception(fmt::format("{} on a {} is only valid for {}", cma.kind, context, type_allowed_str));
}
if (!db.features().writetime_ttl_individual_element) {
throw exceptions::invalid_request_exception(fmt::format(
"{} on a {} is not supported until all nodes in the cluster are upgraded", cma.kind, context));
}
return column_mutation_attribute{.kind = cma.kind, .column = std::move(column)};
};
// Handle WRITETIME(m[key]) / TTL(m[key]) - a subscript into a non-frozen map or set column
if (auto sub = expr::as_if<subscript>(&column)) {
return validate_and_return(sub->val, "subscript",
[](const data_type& t) { return (t->is_map() || t->is_set()) && t->is_multi_cell(); },
"non-frozen map or set columns");
}
// Handle WRITETIME(x.field) / TTL(x.field) - a field selection into a non-frozen UDT column
if (auto fs = expr::as_if<field_selection>(&column)) {
return validate_and_return(fs->structure, "field selection",
[](const data_type& t) { return t->is_user_type() && t->is_multi_cell(); },
"non-frozen UDT columns");
}
auto cval = expr::as_if<column_value>(&column);
if (!cval) {
throw exceptions::invalid_request_exception(fmt::format("{} expects a column, but {} is a general expression", cma.kind, column));