diff --git a/cql3/functions/castas_fcts.cc b/cql3/functions/castas_fcts.cc index a2e16e222125..7f1c1a12b572 100644 --- a/cql3/functions/castas_fcts.cc +++ b/cql3/functions/castas_fcts.cc @@ -165,8 +165,6 @@ static data_value castas_fctn_from_dv_to_string(data_value from) { return from.type()->to_string_impl(from); } -// FIXME: Add conversions for counters, after they are fully implemented... - static constexpr unsigned next_power_of_2(unsigned val) { unsigned ret = 1; while (ret <= val) { @@ -370,6 +368,26 @@ castas_fctn get_castas_fctn(data_type to_type, data_type from_type) { return castas_fctn_from_dv_to_string; case cast_switch_case_val(kind::utf8, kind::ascii): return castas_fctn_simple; + + case cast_switch_case_val(kind::byte, kind::counter): + return castas_fctn_simple; + case cast_switch_case_val(kind::short_kind, kind::counter): + return castas_fctn_simple; + case cast_switch_case_val(kind::int32, kind::counter): + return castas_fctn_simple; + case cast_switch_case_val(kind::long_kind, kind::counter): + return castas_fctn_simple; + case cast_switch_case_val(kind::float_kind, kind::counter): + return castas_fctn_simple; + case cast_switch_case_val(kind::double_kind, kind::counter): + return castas_fctn_simple; + case cast_switch_case_val(kind::varint, kind::counter): + return castas_fctn_simple; + case cast_switch_case_val(kind::decimal, kind::counter): + return castas_fctn_from_integer_to_decimal; + case cast_switch_case_val(kind::ascii, kind::counter): + case cast_switch_case_val(kind::utf8, kind::counter): + return castas_fctn_to_string; } throw exceptions::invalid_request_exception(format("{} cannot be cast to {}", from_type->name(), to_type->name())); } diff --git a/cql3/functions/functions.cc b/cql3/functions/functions.cc index fe7b1dc31153..1d3ac00b5fc5 100644 --- a/cql3/functions/functions.cc +++ b/cql3/functions/functions.cc @@ -105,11 +105,6 @@ functions::init() noexcept { if (type == cql3_type::blob) { continue; } - // counters are not supported yet - if (type.is_counter()) { - warn(unimplemented::cause::COUNTERS); - continue; - } declare(make_to_blob_function(type.get_type())); declare(make_from_blob_function(type.get_type())); diff --git a/test/cql-pytest/cassandra_tests/functions/cast_fcts_test.py b/test/cql-pytest/cassandra_tests/functions/cast_fcts_test.py index 52eac5448a41..e59f887a7f89 100644 --- a/test/cql-pytest/cassandra_tests/functions/cast_fcts_test.py +++ b/test/cql-pytest/cassandra_tests/functions/cast_fcts_test.py @@ -243,7 +243,7 @@ def testCastsWithReverseOrder(cql, test_keyspace): #assertRows(execute(cql, table, "SELECT CAST(" + f + "(CAST(b AS int)) AS text) FROM %s"), # row("2.0")) -@pytest.mark.xfail(reason="issue #14501") +# Reproduces #14501: def testCounterCastsInSelectionClause(cql, test_keyspace): with create_table(cql, test_keyspace, "(a int primary key, b counter)") as table: execute(cql, table, "UPDATE %s SET b = b + 2 WHERE a = 1") diff --git a/test/cql-pytest/test_cast_data.py b/test/cql-pytest/test_cast_data.py index 5d475ee25151..55f350c17ab7 100644 --- a/test/cql-pytest/test_cast_data.py +++ b/test/cql-pytest/test_cast_data.py @@ -28,6 +28,16 @@ def table1(cql, test_keyspace): with new_test_table(cql, test_keyspace, "p int PRIMARY KEY, cVarint varint") as table: yield table +@pytest.fixture(scope="module") +def table2(cql, test_keyspace): + with new_test_table(cql, test_keyspace, "p int PRIMARY KEY, c counter") as table: + yield table + +@pytest.fixture(scope="module") +def table3(cql, test_keyspace): + with new_test_table(cql, test_keyspace, "p int PRIMARY KEY, i int, a ascii, bi bigint, b blob, bool boolean, d date, dec decimal, db double, dur duration, f float, addr inet, si smallint, t text, tim time, ts timestamp, tu timeuuid, ti tinyint, u uuid, vc varchar, vi varint") as table: + yield table + # Utility function for emulating a wrapping cast of a big positive number # into a smaller signed integer of given number of bits. For example, # casting 511 to 8 bits results in -1. @@ -89,4 +99,56 @@ def test_cast_from_large_varint_to_varchar(cql, table1, cassandra_bug): cql.execute(f'INSERT INTO {table1} (p, cVarint) VALUES ({p}, {v})') assert [(str(v),)] == list(cql.execute(f"SELECT CAST(cVarint AS varchar) FROM {table1} WHERE p={p}")) +# Test casting a counter to various other types. Reproduces #14501. +def test_cast_from_counter(cql, table2): + p = unique_key_int() + # Set the counter to 1000 in two increments, to make it less trivial to + # read correctly. + cql.execute(f'UPDATE {table2} SET c = c + 230 WHERE p = {p}') + cql.execute(f'UPDATE {table2} SET c = c + 770 WHERE p = {p}') + # We can read back the original number without a cast, or with a silly + # cast to the same type "counter". + assert [(1000,)] == list(cql.execute(f"SELECT c FROM {table2} WHERE p={p}")) + assert [(1000,)] == list(cql.execute(f"SELECT CAST(c AS counter) FROM {table2} WHERE p={p}")) + # Casting into smaller integer types results in wraparound + assert [(signed(1000,8),)] == list(cql.execute(f"SELECT CAST(c AS tinyint) FROM {table2} WHERE p={p}")) + assert [(1000,)] == list(cql.execute(f"SELECT CAST(c AS smallint) FROM {table2} WHERE p={p}")) + assert [(1000,)] == list(cql.execute(f"SELECT CAST(c AS int) FROM {table2} WHERE p={p}")) + assert [(1000,)] == list(cql.execute(f"SELECT CAST(c AS bigint) FROM {table2} WHERE p={p}")) + assert [(1000.0,)] == list(cql.execute(f"SELECT CAST(c AS float) FROM {table2} WHERE p={p}")) + assert [(1000.0,)] == list(cql.execute(f"SELECT CAST(c AS double) FROM {table2} WHERE p={p}")) + # Casting the counter to string types results in printing the number in + # decimal, as expected + assert [("1000",)] == list(cql.execute(f"SELECT CAST(c AS ascii) FROM {table2} WHERE p={p}")) + assert [("1000",)] == list(cql.execute(f"SELECT CAST(c AS text) FROM {table2} WHERE p={p}")) + # "varchar" is supposed to be an alias to "text" and should work, but + # suprisingly casting to varchar doesn't work on Cassandra, so we + # test it in a separate test below, test_cast_from_counter_to_varchar. + # Casting a counter to all other types is NOT allowed: + for t in ['blob', 'boolean', 'date', 'duration', 'inet', + 'timestamp', 'timeuuid', 'uuid']: + with pytest.raises(InvalidRequest, match='cast'): + cql.execute(f"SELECT CAST(c AS {t}) FROM {table2} WHERE p={p}") + +# In test_cast_from_counter we checked that a counter can be cast to the +# "text" type. Since "varchar" is just an alias for "text", casting +# to varchar should work too, but in Cassandra it doesn't so this test +# is marked a Cassandra bug. +def test_cast_from_counter_to_varchar(cql, table2, cassandra_bug): + p = unique_key_int() + cql.execute(f'UPDATE {table2} SET c = c + 1000 WHERE p = {p}') + assert [("1000",)] == list(cql.execute(f"SELECT CAST(c AS varchar) FROM {table2} WHERE p={p}")) + +# Test casts from various types *to* counter type. This is a rather silly +# operation - casting to "counter" doesn't make a real counter. It could +# have been supported the same as casting to bigint, but Cassandra chose not +# to support it and neither do we. +# The only case that works is the do-nothing casting of a counter to counter, +# and that case is already checked in test_cast_from_counter(). +def test_cast_to_counter(cql, table3): + p = unique_key_int() + for col in ['i', 'a', 'bi', 'b', 'bool', 'd', 'dec', 'db', 'dur', 'f', 'addr', 'si', 't', 'tim', 'ts', 'tu', 'ti', 'u', 'vc', 'vi']: + with pytest.raises(InvalidRequest, match='cannot be cast'): + cql.execute(f"SELECT CAST({col} AS counter) FROM {table3} WHERE p={p}") + # TODO: test casts from more types. diff --git a/test/cql-pytest/test_counter.py b/test/cql-pytest/test_counter.py new file mode 100644 index 000000000000..7facfa253af9 --- /dev/null +++ b/test/cql-pytest/test_counter.py @@ -0,0 +1,66 @@ +# Copyright 2023-present ScyllaDB +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +############################################################################### +# Tests for various operations on COUNTER columns. +# See also tests for casting involving counter columns in test_cast_data.py +############################################################################### + +import pytest +from util import new_test_table, unique_key_int +from cassandra.protocol import InvalidRequest + +@pytest.fixture(scope="module") +def table1(cql, test_keyspace): + with new_test_table(cql, test_keyspace, "p int PRIMARY KEY, i bigint, v int") as table: + yield table + +@pytest.fixture(scope="module") +def table2(cql, test_keyspace): + with new_test_table(cql, test_keyspace, "p int PRIMARY KEY, c counter") as table: + yield table + +# Test that the function counterasblob() exists and works as expected - +# same as bigintasblob on the same number (a counter is a 64-bit number). +# Reproduces #14742 +def test_counter_to_blob(cql, table1, table2): + p = unique_key_int() + cql.execute(f'UPDATE {table1} SET i = 1000 WHERE p = {p}') + cql.execute(f'UPDATE {table2} SET c = c + 1000 WHERE p = {p}') + expected = b'\x00\x00\x00\x00\x00\x00\x03\xe8' + assert [(expected,)] == list(cql.execute(f"SELECT bigintasblob(i) FROM {table1} WHERE p={p}")) + assert [(expected,)] == list(cql.execute(f"SELECT counterasblob(c) FROM {table2} WHERE p={p}")) + # Although the representation of the counter and bigint types are the + # same (64-bit), you can't use the wrong "*asblob()" function: + with pytest.raises(InvalidRequest, match='counterasblob'): + cql.execute(f"SELECT counterasblob(i) FROM {table1} WHERE p={p}") + # The opposite order is allowed in Scylla because of #14319, so let's + # split it into a second test test_counter_to_blob2: +@pytest.mark.xfail(reason="issue #14319") +def test_counter_to_blob2(cql, table1, table2): + p = unique_key_int() + cql.execute(f'UPDATE {table2} SET c = c + 1000 WHERE p = {p}') + # Reproduces #14319: + with pytest.raises(InvalidRequest, match='bigintasblob'): + cql.execute(f"SELECT bigintasblob(c) FROM {table2} WHERE p={p}") + +# Test that the function blobascounter() exists and works as expected. +# Reproduces #14742 +def test_counter_from_blob(cql, table1): + p = unique_key_int() + cql.execute(f'UPDATE {table1} SET i = 1000 WHERE p = {p}') + assert [(1000,)] == list(cql.execute(f"SELECT blobascounter(bigintasblob(i)) FROM {table1} WHERE p={p}")) + +# blobascounter() must insist to receive a properly-sized (8-byte) blob. +# If it accepts a shorter blob (e.g., 4 bytes) and returns that to the driver, +# it will confuse the driver (the driver will expect to read 8 bytes for the +# bigint but will get only 4). +# We have test_native_functions.py::test_blobas_wrong_size() that verifies +# that this protection works for the bigint type, but it turns out it also +# needs to be separately enforced for the counter type. +def test_blobascounter_wrong_size(cql, table1): + p = unique_key_int() + cql.execute(f'UPDATE {table1} SET v = 1000 WHERE p = {p}') + with pytest.raises(InvalidRequest, match='blobascounter'): + cql.execute(f"SELECT blobascounter(intasblob(v)) FROM {table1} WHERE p={p}") diff --git a/test/cql-pytest/test_native_functions.py b/test/cql-pytest/test_native_functions.py new file mode 100644 index 000000000000..a07cfa8cfaa7 --- /dev/null +++ b/test/cql-pytest/test_native_functions.py @@ -0,0 +1,74 @@ +# Copyright 2023-present ScyllaDB +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +############################################################################### +# Tests for various native (built-in) scalar functions that can be used in +# various SELECT, INSERT or UPDATE requests. Note we also have tests for +# some of these functions in many other test files. For example, the tests +# for the cast() function are in test_cast_data.py. +############################################################################### + +import pytest +from util import new_test_table, unique_key_int +from cassandra.protocol import InvalidRequest + +@pytest.fixture(scope="module") +def table1(cql, test_keyspace): + with new_test_table(cql, test_keyspace, "p int, i int, g bigint, b blob, s text, t timestamp, u timeuuid, PRIMARY KEY (p)") as table: + yield table + +# Check that a function that can take a column name as a parameter, can also +# take a constant. This feature is barely useful for WHERE clauses, and +# even less useful for selectors, but should be allowed for both. +# Reproduces #12607. +@pytest.mark.xfail(reason="issue #12607") +def test_constant_function_parameter(cql, table1): + p = unique_key_int() + cql.execute(f"INSERT INTO {table1} (p, b) VALUES ({p}, 0x03)") + assert [(p,)] == list(cql.execute(f"SELECT p FROM {table1} WHERE p={p} AND b=tinyintAsBlob(3) ALLOW FILTERING")) + assert [(b'\x04',)] == list(cql.execute(f"SELECT tinyintAsBlob(4) FROM {table1} WHERE p={p}")) + +# According to the documentation, "The `minTimeuuid` function takes a +# `timestamp` value t, either a timestamp or a date string.". But although +# both cases are supported with constant parameters in WHERE restrictions, +# in a *selector* (the first part of the SELECT, saying what to select), it +# turns out that ONLY a timestamp column is allowed. Although this is +# undocumented behavior, both Cassandra and Scylla share it so we deem it +# correct. +def test_selector_mintimeuuid(cql, table1): + p = unique_key_int() + cql.execute(f"INSERT INTO {table1} (p, s, t, i) VALUES ({p}, '2013-02-02 10:00+0000', 123, 456)") + # We just check this works, not what the value is: + cql.execute(f"SELECT mintimeuuid(t) FROM {table1} WHERE p={p}") + # This doesn't work - despite the documentation, in a selector a + # date string is not supported by mintimeuuid. + with pytest.raises(InvalidRequest, match='of type timestamp'): + cql.execute(f"SELECT mintimeuuid(s) FROM {table1} WHERE p={p}") + # Other integer types also don't work, it must be a timestamp: + with pytest.raises(InvalidRequest, match='of type timestamp'): + cql.execute(f"SELECT mintimeuuid(i) FROM {table1} WHERE p={p}") + +# Cassandra allows the implicit (and wrong!) casting of a bigint returned +# by writetime() to the timestamp type required by mintimeuuid(). Scylla +# doesn't. I'm not sure which behavior we should consider correct, but it's +# useful to have a test that demonstrates this incompatibility. +# Reproduces #14319. +@pytest.mark.xfail(reason="issue #14319") +def test_selector_mintimeuuid_64bit(cql, table1): + p = unique_key_int() + cql.execute(f"INSERT INTO {table1} (p, g) VALUES ({p}, 123)") + cql.execute(f"SELECT mintimeuuid(g) FROM {table1} WHERE p={p}") + cql.execute(f"SELECT mintimeuuid(writetime(g)) FROM {table1} WHERE p={p}") + +# blobasbigint() must insist to receive a properly-sized (8-byte) blob. +# If it accepts a shorter blob (e.g., 4 bytes) and returns that to the driver, +# it will confuse the driver (the driver will expect to read 8 bytes for the +# bigint but will get only 4). +def test_blobas_wrong_size(cql, table1): + p = unique_key_int() + cql.execute(f"INSERT INTO {table1} (p, i) VALUES ({p}, 123)") + # Cassandra and Scylla print: "In call to function system.blobasbigint, + # value 0x0000007b is not a valid binary representation for type bigint". + with pytest.raises(InvalidRequest, match='blobasbigint'): + cql.execute(f"SELECT blobasbigint(intasblob(i)) FROM {table1} WHERE p={p}") diff --git a/types/types.cc b/types/types.cc index f0f6ed02b2d5..1c346a336caa 100644 --- a/types/types.cc +++ b/types/types.cc @@ -1576,6 +1576,9 @@ struct validate_visitor { void operator()(const reversed_type_impl& t) { visit(*t.underlying_type(), validate_visitor{v}); } + void operator()(const counter_type_impl&) { + visit(*long_type, validate_visitor{v}); + } void operator()(const abstract_type&) {} template void operator()(const integer_type_impl& t) { if (v.empty()) { @@ -1826,6 +1829,7 @@ struct serialize_visitor { bytes::iterator& out; ; void operator()(const reversed_type_impl& t, const void* v) { return serialize(*t.underlying_type(), v, out); } + void operator()(const counter_type_impl& t, const void* v) { return serialize(*long_type, v, out); } template void operator()(const integer_type_impl& t, const typename integer_type_impl::native_type* v1) { if (v1->empty()) { @@ -1936,7 +1940,6 @@ struct serialize_visitor { out = std::copy_n(reinterpret_cast(&u), sizeof(int32_t), out); serialize_varint(out, bd.unscaled_value()); } - void operator()(const counter_type_impl& t, const void*) { fail(unimplemented::cause::COUNTERS); } void operator()(const duration_type_impl& t, const duration_type_impl::native_type* m) { if (m->empty()) { return; @@ -2532,6 +2535,7 @@ size_t abstract_type::hash(managed_bytes_view v) const { struct visitor { managed_bytes_view v; size_t operator()(const reversed_type_impl& t) { return t.underlying_type()->hash(v); } + size_t operator()(const counter_type_impl&) { return long_type->hash(v); } size_t operator()(const abstract_type& t) { return std::hash()(v); } size_t operator()(const tuple_type_impl& t) { auto apply_hash = [] (auto&& type_value) { @@ -2549,7 +2553,6 @@ size_t abstract_type::hash(managed_bytes_view v) const { size_t operator()(const decimal_type_impl& t) { return std::hash()(with_linearized(v, [&] (bytes_view bv) { return t.to_string(bv); })); } - size_t operator()(const counter_type_impl&) { fail(unimplemented::cause::COUNTERS); } size_t operator()(const empty_type_impl&) { return 0; } }; return visit(*this, visitor{v}); @@ -2617,6 +2620,7 @@ static size_t serialized_size(const abstract_type& t, const void* value); namespace { struct serialized_size_visitor { size_t operator()(const reversed_type_impl& t, const void* v) { return serialized_size(*t.underlying_type(), v); } + size_t operator()(const counter_type_impl&, const void* v) { return serialized_size(*long_type, v); } size_t operator()(const empty_type_impl&, const void*) { return 0; } template size_t operator()(const concrete_type& t, const typename concrete_type::native_type* v) { @@ -2625,7 +2629,6 @@ struct serialized_size_visitor { } return concrete_serialized_size(*v); } - size_t operator()(const counter_type_impl&, const void*) { fail(unimplemented::cause::COUNTERS); } size_t operator()(const map_type_impl& t, const map_type_impl::native_type* v) { return map_serialized_size(v); } size_t operator()(const concrete_type, listlike_collection_type_impl>& t, const std::vector* v) { @@ -2678,6 +2681,7 @@ namespace { struct from_string_visitor { sstring_view s; bytes operator()(const reversed_type_impl& r) { return r.underlying_type()->from_string(s); } + bytes operator()(const counter_type_impl&) { return long_type->from_string(s); } template bytes operator()(const integer_type_impl& t) { return decompose_value(parse_int(t, s)); } bytes operator()(const ascii_type_impl&) { auto bv = bytes_view(reinterpret_cast(s.begin()), s.size()); @@ -2768,10 +2772,6 @@ struct from_string_visitor { throw marshal_exception(format("unable to make BigDecimal from '{}'", s)); } } - bytes operator()(const counter_type_impl&) { - fail(unimplemented::cause::COUNTERS); - return bytes(); - } bytes operator()(const duration_type_impl& t) { if (s.empty()) { return bytes(); @@ -2868,7 +2868,6 @@ struct to_string_impl_visitor { sstring operator()(const boolean_type_impl& b, const boolean_type_impl::native_type* v) { return format_if_not_empty(b, v, boolean_to_string); } - sstring operator()(const counter_type_impl& c, const void*) { fail(unimplemented::cause::COUNTERS); } sstring operator()(const timestamp_date_base_class& d, const timestamp_date_base_class::native_type* v) { return format_if_not_empty(d, v, [] (const db_clock::time_point& v) { return time_point_to_string(v); }); } @@ -2894,6 +2893,7 @@ struct to_string_impl_visitor { m, v, [&m] (const map_type_impl::native_type& v) { return map_to_string(v, !m.is_multi_cell()); }); } sstring operator()(const reversed_type_impl& r, const void* v) { return to_string_impl(*r.underlying_type(), v); } + sstring operator()(const counter_type_impl& c, const void* v) { return to_string_impl(*long_type, v); } sstring operator()(const simple_date_type_impl& s, const simple_date_type_impl::native_type* v) { return format_if_not_empty(s, v, simple_date_to_string); } @@ -3052,11 +3052,13 @@ struct native_value_clone_visitor { void* operator()(const reversed_type_impl& t) { return visit(*t.underlying_type(), native_value_clone_visitor{from}); } + void* operator()(const counter_type_impl& t) { + return visit(*long_type, native_value_clone_visitor{from}); + } template void* operator()(const concrete_type&) { using nt = typename concrete_type::native_type; return new nt(*reinterpret_cast(from)); } - void* operator()(const counter_type_impl&) { fail(unimplemented::cause::COUNTERS); } void* operator()(const empty_type_impl&) { return new empty_type_representation(); } @@ -3076,7 +3078,9 @@ struct native_value_delete_visitor { void operator()(const reversed_type_impl& t) { return visit(*t.underlying_type(), native_value_delete_visitor{object}); } - void operator()(const counter_type_impl&) { fail(unimplemented::cause::COUNTERS); } + void operator()(const counter_type_impl& t) { + return visit(*long_type, native_value_delete_visitor{object}); + } void operator()(const empty_type_impl&) { delete reinterpret_cast(object); } @@ -3095,7 +3099,9 @@ struct native_typeid_visitor { const std::type_info& operator()(const reversed_type_impl& t) { return visit(*t.underlying_type(), native_typeid_visitor{}); } - const std::type_info& operator()(const counter_type_impl&) { fail(unimplemented::cause::COUNTERS); } + const std::type_info& operator()(const counter_type_impl& t) { + return visit(*long_type, native_typeid_visitor{}); + } const std::type_info& operator()(const empty_type_impl&) { // Can't happen abort();