From 33dbb63aefd1414cacc2041cf243892d92a6dc12 Mon Sep 17 00:00:00 2001 From: Nadav Har'El Date: Thu, 26 Mar 2026 11:26:32 +0200 Subject: [PATCH] cql3: support UDT fields in LWT expressions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In an earlier patch, we used the CQL grammar's "subscriptExpr" in the rule for WRITETIME() and TTL(). But since we also wanted these to support UDT fields (x.a), not just collection subscripts (x[3]), we expanded subscriptExpr to also support the field syntax. But LWT expressions already used this subscriptExpr, which meant that LWT expressions unintentionally gained support for UDT fields. Missing support for UDT fields in LWT is a long-standing known Cassandra-compatibility bug (#13624), and now our grammar finally supports the missing syntax. But supporting the syntax is not enough for correct implementation of this feature - we also need to fix the expression handling: Two bugs prevented expressions like `v.a = 0` from working in LWT IF clauses, where `v` is a column of user-defined type. The first bug was in get_lhs_receiver() in prepare_expr.cc: it lacked a handler for field_selection nodes, causing an "unexpected expression" internal error when preparing a condition like `IF v.a = 0`. The fix adds a handler that returns a column_specification whose type is taken from the prepared field_selection's type field. The second bug was in search_and_replace() in expression.cc: when recursing into a field_selection node it reconstructed it with only `structure` and `field`, silently dropping the `field_idx` and `type` fields that are set during preparation. As a result, any transformation that uses search_and_replace() on a prepared expression containing a field_selection — such as adjust_for_collection_as_maps() called from column_condition_prepare() — would zero out those fields. At evaluation time, type_of() on the field_selection returned a null data_type pointer, causing a segmentation fault when the comparison operator tried to call ->equal() through it. The fix preserves field_idx and type when reconstructing the node. Fixes #13624. --- cql3/expr/expression.cc | 2 +- cql3/expr/prepare_expr.cc | 6 ++++++ .../insert_update_if_condition_collections_test.py | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/cql3/expr/expression.cc b/cql3/expr/expression.cc index 7c7d5260d1..ab5e1bd1c4 100644 --- a/cql3/expr/expression.cc +++ b/cql3/expr/expression.cc @@ -1031,7 +1031,7 @@ expression search_and_replace(const expression& e, return cast{c.style, recurse(c.arg), c.type}; }, [&] (const field_selection& fs) -> expression { - return field_selection{recurse(fs.structure), fs.field}; + return field_selection{recurse(fs.structure), fs.field, fs.field_idx, fs.type}; }, [&] (const subscript& s) -> expression { return subscript { diff --git a/cql3/expr/prepare_expr.cc b/cql3/expr/prepare_expr.cc index 8b057bef01..b34f6f6f4b 100644 --- a/cql3/expr/prepare_expr.cc +++ b/cql3/expr/prepare_expr.cc @@ -1688,6 +1688,12 @@ static lw_shared_ptr get_lhs_receiver(const expression& pr return list_value_spec_of(*sub_col.col->column_specification); } }, + [&](const field_selection& fs) -> lw_shared_ptr { + return make_lw_shared( + schema.ks_name(), schema.cf_name(), + ::make_shared(fs.field->text(), true), + fs.type); + }, [&](const tuple_constructor& tup) -> lw_shared_ptr { std::ostringstream tuple_name; tuple_name << "("; diff --git a/test/cqlpy/cassandra_tests/validation/operations/insert_update_if_condition_collections_test.py b/test/cqlpy/cassandra_tests/validation/operations/insert_update_if_condition_collections_test.py index af3aa8baa4..f6ed5dc3fa 100644 --- a/test/cqlpy/cassandra_tests/validation/operations/insert_update_if_condition_collections_test.py +++ b/test/cqlpy/cassandra_tests/validation/operations/insert_update_if_condition_collections_test.py @@ -266,7 +266,7 @@ def checkInvalidUDT(cql, table, condition, value, expected): assertInvalidThrow(cql, table, expected, "DELETE FROM %s WHERE k = 0 IF " + condition) assertRows(execute(cql, table, "SELECT * FROM %s"), row(0, value)) -@pytest.mark.xfail(reason="Issue #13624") +# Reproduces #13624 def testUDTField(cql, test_keyspace): with create_type(cql, test_keyspace, "(a int, b text)") as typename: for frozen in [False, True]: