From 35e807a36ceff76c529626058383a3fe71f7e3fd Mon Sep 17 00:00:00 2001 From: Nadav Har'El Date: Sun, 12 Apr 2026 11:06:59 +0300 Subject: [PATCH] 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. --- cql3/expr/expression.cc | 52 +++++++++++++++++++++++++++++++++++++++ cql3/expr/prepare_expr.cc | 34 +++++++++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/cql3/expr/expression.cc b/cql3/expr/expression.cc index fe3e1105c9..7c7d5260d1 100644 --- a/cql3/expr/expression.cc +++ b/cql3/expr/expression.cc @@ -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(&cma.column)) { + auto inner_col = expr::as_if(&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(&cma.column)) { + auto inner_col = expr::as_if(&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(&cma.column); if (!col) { on_internal_error(expr_logger, fmt::format("evaluating column_mutation_attribute of non-column {}", cma.column)); diff --git a/cql3/expr/prepare_expr.cc b/cql3/expr/prepare_expr.cc index c743ea3172..8b057bef01 100644 --- a/cql3/expr/prepare_expr.cc +++ b/cql3/expr/prepare_expr.cc @@ -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 { + auto inner_cval = expr::as_if(&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(&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(&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); if (!cval) { throw exceptions::invalid_request_exception(fmt::format("{} expects a column, but {} is a general expression", cma.kind, column));