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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user