cql3: accept and type-check reused named bind variables

A named bind-variable can be reused:

    SELECT * FROM tab
    WHERE a = :var AND b = :var

Currently, the grammar just ignores the possibility and creates
a new variable with the same name. The new variable cannot be
referenced by name since the first one shadows it.

Catch variable reuse by maintaining a map from bind variable names
to indexed, and check that when reusing a bind variable the types
match.

A unit test is added.

Fixes #10810

Closes #10813
This commit is contained in:
Avi Kivity
2022-06-15 19:43:22 +03:00
committed by Piotr Sarna
parent 670b2562a1
commit 19a6e69001
3 changed files with 38 additions and 1 deletions

View File

@@ -134,6 +134,8 @@ struct uninitialized {
listener_type* listener;
std::vector<::shared_ptr<cql3::column_identifier>> _bind_variables;
// index into _bind_variables
std::unordered_map<cql3::column_identifier, size_t> _named_bind_variables_indexes;
std::vector<std::unique_ptr<TokenType>> _missing_tokens;
// Can't use static variable, since it needs to be defined out-of-line
@@ -153,8 +155,14 @@ struct uninitialized {
bind_variable new_bind_variables(shared_ptr<cql3::column_identifier> name)
{
if (name && _named_bind_variables_indexes.contains(*name)) {
return bind_variable{_named_bind_variables_indexes[*name]};
}
auto marker = bind_variable{_bind_variables.size()};
_bind_variables.push_back(name);
if (name) {
_named_bind_variables_indexes[*name] = marker.bind_index;
}
return marker;
}

View File

@@ -47,8 +47,16 @@ std::vector<uint16_t> prepare_context::get_partition_key_bind_indexes(const sche
}
void prepare_context::add_variable_specification(int32_t bind_index, lw_shared_ptr<column_specification> spec) {
_target_columns[bind_index] = spec;
auto name = _variable_names[bind_index];
if (_specs[bind_index]) {
// If the same variable is used in multiple places, check that the types are compatible
if (&spec->type->without_reversed() != &_specs[bind_index]->type->without_reversed()) {
throw exceptions::invalid_request_exception(
fmt::format("variable :{} has type {} which doesn't match {}",
*name, _specs[bind_index]->type->as_cql3_type(), spec->name));
}
}
_target_columns[bind_index] = spec;
// Use the user name, if there is one
if (name) {
spec = make_lw_shared<column_specification>(spec->ks_name, spec->cf_name, name, spec->type);

View File

@@ -5321,3 +5321,24 @@ SEASTAR_TEST_CASE(test_null_and_unset_in_collections) {
exceptions::invalid_request_exception, check_unset_msg());
});
}
SEASTAR_TEST_CASE(test_bind_variable_type_checking) {
return do_with_cql_env_thread([](cql_test_env& e) {
e.execute_cql("CREATE TABLE tab1 (p int primary key, a int, b text, c int)").get();
// The predicate that checks the message has to be a lambda to preserve source_location
auto check_type_conflict = [](std::experimental::source_location loc = std::experimental::source_location::current()) {
return exception_predicate::message_contains("variable :var has type", loc);
};
// Test :var needing to have two conflicting types
BOOST_REQUIRE_EXCEPTION(e.prepare("INSERT INTO tab1 (p, a, b) VALUES (0, :var, :var)").get(),
exceptions::invalid_request_exception, check_type_conflict());
BOOST_REQUIRE_EXCEPTION(e.prepare("SELECT * FROM tab1 WHERE a = :var AND b = :var ALLOW FILTERING").get(),
exceptions::invalid_request_exception, check_type_conflict());
// Test :var with a compatible type
e.prepare("INSERT INTO tab1 (p, a, c) VALUES (0, :var, :var)").get();
e.prepare("SELECT * FROM tab1 WHERE a = :var AND c = :var ALLOW FILTERING").get();
});
}