mirror of
https://github.com/scylladb/scylladb.git
synced 2026-05-02 22:25:48 +00:00
If the index was created on collection (both frozen or not), its description wasn't a correct create statement. This patch fixes the bug and includes functions like `full()`, `keys()`, `values()`, ... used to create index on collections. Fixes scylladb/scylladb#19278 Closes scylladb/scylladb#19381 * github.com:scylladb/scylladb: cql-pytest/test_describe: add a test for describe indexes schema/schema: fix column names in index description
1196 lines
56 KiB
Python
1196 lines
56 KiB
Python
|
|
# Copyright 2022-present ScyllaDB
|
|
#
|
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
###############################################################################
|
|
# Tests for server-side describe
|
|
###############################################################################
|
|
|
|
import pytest
|
|
import random
|
|
from pytest import fixture
|
|
from contextlib import contextmanager, ExitStack
|
|
from util import new_type, unique_name, new_test_table, new_test_keyspace, new_function, new_aggregate, new_cql, keyspace_has_tablets, unique_name_prefix
|
|
from cassandra.protocol import InvalidRequest
|
|
import re
|
|
|
|
# (`element` refers to keyspace or keyspace's element(table, type, function, aggregate))
|
|
# There are 2 main types of tests:
|
|
# - tests for listings (DESC TABLES/DESC KEYSPACES/DESC TYPES/...)
|
|
# Those tests lists elements in two ways. First the elements are listed
|
|
# using describe statement, second the proper table from `system_schema`
|
|
# is queried. Each row from the table should be present in result from
|
|
# describe statement.
|
|
# e.g.: Result of `DESC TABLE` is compared to
|
|
# `SELECT keyspace_name, table_name FROM system_schema.tables`
|
|
#
|
|
# - tests for element's description (aka create statement)
|
|
# Those tests are expected to validate the create statement of an element,
|
|
# in other words, element created by copy-pasting description should be
|
|
# identical to the original one.
|
|
# The similarity is validated by comparing rows from tables
|
|
# in `system_scehma` keyspaces (excluding elements' name).
|
|
# In order to cover all possibilities, the elements are created randomly.
|
|
#
|
|
# There are also single tests for DESC CLUSTER and DESC SCHEMA statements and also
|
|
# for some corner cases.
|
|
|
|
# Test `DESC {elements}` listing. Assert if all rows from `system_schema.{elements}`
|
|
# are in describe result
|
|
def assert_element_listing(cql, elements, name_column, name_f):
|
|
# Create new session to ensure no `USE keyspace` statement was executed before
|
|
with new_cql(cql) as ncql:
|
|
table_result = ncql.execute(f"SELECT keyspace_name, {name_column} FROM system_schema.{elements}")
|
|
desc_result = ncql.execute(f"DESC {elements}")
|
|
|
|
desc_listing = [(r.keyspace_name, r.name) for r in desc_result]
|
|
|
|
for row in table_result:
|
|
assert (maybe_quote(row.keyspace_name), maybe_quote(name_f(row))) in desc_listing
|
|
|
|
# Test that `DESC KEYSPACES` contains all keyspaces
|
|
def test_keyspaces(cql, test_keyspace):
|
|
assert_element_listing(cql, "keyspaces", "keyspace_name", lambda r: r.keyspace_name)
|
|
|
|
# Test that `DESC TABLES` contains all tables
|
|
def test_tables(cql, test_keyspace):
|
|
with new_test_table(cql, test_keyspace, "id int PRIMARY KEY"):
|
|
assert_element_listing(cql, "tables", "table_name", lambda r: r.table_name)
|
|
|
|
# Test that `DESC TYPES` contains all user types
|
|
def test_desc_types(cql, test_keyspace):
|
|
with new_random_type(cql, test_keyspace), new_random_type(cql, test_keyspace):
|
|
assert_element_listing(cql, "types", "type_name", lambda r: r.type_name)
|
|
|
|
# Test that `DESC FUNCTIONS` contains all user functions
|
|
def test_desc_functions(scylla_only, cql, test_keyspace):
|
|
fn_schema = "(val int)"
|
|
fn_null = "RETURNS NULL ON NULL INPUT"
|
|
fn_return = "RETURNS int"
|
|
fn_language = "LANGUAGE lua"
|
|
fn_body = "AS 'return -val'"
|
|
with new_function(cql, test_keyspace, f"{fn_schema} {fn_null} {fn_return} {fn_language} {fn_body}"):
|
|
assert_element_listing(cql, "functions", "function_name", lambda r: r.function_name)
|
|
|
|
# Test that `DESC AGGREGATES` contains all user aggregates
|
|
def test_desc_aggregates(scylla_only, cql, test_keyspace):
|
|
fn_schema = "(val1 int, val2 int)"
|
|
fn_null = "RETURNS NULL ON NULL INPUT"
|
|
fn_return = "RETURNS int"
|
|
fn_language = "LANGUAGE lua"
|
|
fn_body = "return val1 + val2"
|
|
|
|
with new_function(cql, test_keyspace, f"{fn_schema} {fn_null} {fn_return} {fn_language} AS '{fn_body}'") as fn:
|
|
agg_body = f"(int) SFUNC {fn} STYPE int INITCOND 0"
|
|
with new_aggregate(cql, test_keyspace, agg_body):
|
|
assert_element_listing(cql, "aggregates", "aggregate_name", lambda r: r.aggregate_name)
|
|
|
|
# Test that `DESC ONLY KEYSPACE {ks}` contains appropriate create statement for keyspace
|
|
# This test compares the content of `system_schema.keyspaces` table.
|
|
def test_desc_keyspace(cql, random_seed):
|
|
with new_random_keyspace(cql) as ks:
|
|
desc = cql.execute(f"DESC ONLY KEYSPACE {ks}")
|
|
desc_stmt = desc.one().create_statement
|
|
|
|
new_ks = unique_name()
|
|
new_desc_stmt = desc_stmt.replace(ks, new_ks)
|
|
|
|
try:
|
|
cql.execute(new_desc_stmt)
|
|
ks_row = cql.execute(f"""
|
|
SELECT durable_writes, replication
|
|
FROM system_schema.keyspaces
|
|
WHERE keyspace_name='{ks}'
|
|
""").one()
|
|
new_ks_row = cql.execute(f"""
|
|
SELECT durable_writes, replication
|
|
FROM system_schema.keyspaces
|
|
WHERE keyspace_name='{new_ks}'
|
|
""").one()
|
|
assert ks_row == new_ks_row
|
|
finally:
|
|
cql.execute(f"DROP KEYSPACE {new_ks}")
|
|
# Test that `DESC ONLY KEYSPACE {ks}` contains appropriate create statement for keyspace
|
|
# This test compares the content of `system_schema.scylla_keyspaces` table, thus the test
|
|
# is `scylla_only`.
|
|
def test_desc_scylla_keyspace(scylla_only, cql, random_seed):
|
|
with new_random_keyspace(cql) as ks:
|
|
desc = cql.execute(f"DESC ONLY KEYSPACE {ks}")
|
|
desc_stmt = desc.one().create_statement
|
|
|
|
new_ks = unique_name()
|
|
new_desc_stmt = desc_stmt.replace(ks, new_ks)
|
|
|
|
try:
|
|
cql.execute(new_desc_stmt)
|
|
ks_scylla_row = cql.execute(f"""
|
|
SELECT storage_options, storage_type
|
|
FROM system_schema.scylla_keyspaces
|
|
WHERE keyspace_name='{ks}'
|
|
""").one()
|
|
new_ks_scylla_row = cql.execute(f"""
|
|
SELECT storage_options, storage_type
|
|
FROM system_schema.scylla_keyspaces
|
|
WHERE keyspace_name='{new_ks}'
|
|
""").one()
|
|
assert ks_scylla_row == new_ks_scylla_row
|
|
finally:
|
|
cql.execute(f"DROP KEYSPACE {new_ks}")
|
|
|
|
# Test that `DESC TABLE {tbl}` contains appropriate create statement for table
|
|
# This test compares the content of `system_schema.tables` and `system_schema.columns` tables.
|
|
def test_desc_table(cql, test_keyspace, random_seed, has_tablets):
|
|
if has_tablets: # issue #18180
|
|
global counter_table_chance
|
|
counter_table_chance = 0
|
|
with new_random_table(cql, test_keyspace) as tbl:
|
|
desc = cql.execute(f"DESC TABLE {tbl}")
|
|
desc_stmt = desc.one().create_statement
|
|
|
|
new_tbl = f"{test_keyspace}.{unique_name()}"
|
|
new_desc_stmt = desc_stmt.replace(tbl, new_tbl)
|
|
|
|
try:
|
|
cql.execute(new_desc_stmt)
|
|
tbl_row = cql.execute(f"""
|
|
SELECT keyspace_name, bloom_filter_fp_chance, caching,
|
|
comment, compaction, compression, crc_check_chance,
|
|
default_time_to_live, extensions,
|
|
flags, gc_grace_seconds, max_index_interval,
|
|
memtable_flush_period_in_ms, min_index_interval,
|
|
speculative_retry
|
|
FROM system_schema.tables
|
|
WHERE keyspace_name='{test_keyspace}' AND table_name='{get_name(tbl)}'
|
|
""").one()
|
|
new_tbl_row = cql.execute(f"""
|
|
SELECT keyspace_name, bloom_filter_fp_chance, caching,
|
|
comment, compaction, compression, crc_check_chance,
|
|
default_time_to_live, extensions,
|
|
flags, gc_grace_seconds, max_index_interval,
|
|
memtable_flush_period_in_ms, min_index_interval,
|
|
speculative_retry
|
|
FROM system_schema.tables
|
|
WHERE keyspace_name='{test_keyspace}' AND table_name='{get_name(new_tbl)}'
|
|
""").one()
|
|
assert tbl_row == new_tbl_row
|
|
|
|
col_rows = cql.execute(f"""
|
|
SELECT keyspace_name, column_name, clustering_order, column_name_bytes,
|
|
kind, position, type
|
|
FROM system_schema.columns
|
|
WHERE keyspace_name='{test_keyspace}' AND table_name='{get_name(tbl)}'
|
|
""")
|
|
new_col_rows = cql.execute(f"""
|
|
SELECT keyspace_name, column_name, clustering_order, column_name_bytes,
|
|
kind, position, type
|
|
FROM system_schema.columns
|
|
WHERE keyspace_name='{test_keyspace}' AND table_name='{get_name(new_tbl)}'
|
|
""")
|
|
assert col_rows == new_col_rows
|
|
finally:
|
|
cql.execute(f"DROP TABLE {new_tbl}")
|
|
|
|
# Test that `DESC TABLE {tbl}` contains appropriate create statement for table
|
|
# This test compares the content of `system_schema.scylla_tables` tables, thus the test
|
|
# is `scylla_only`.
|
|
def test_desc_scylla_table(scylla_only, cql, test_keyspace, random_seed, has_tablets):
|
|
if has_tablets: # issue #18180
|
|
global counter_table_chance
|
|
counter_table_chance = 0
|
|
with new_random_table(cql, test_keyspace) as tbl:
|
|
desc = cql.execute(f"DESC TABLE {tbl}")
|
|
desc_stmt = desc.one().create_statement
|
|
|
|
new_tbl = f"{test_keyspace}.{unique_name()}"
|
|
new_desc_stmt = desc_stmt.replace(tbl, new_tbl)
|
|
|
|
try:
|
|
cql.execute(new_desc_stmt)
|
|
tbl_scylla_row = cql.execute(f"""
|
|
SELECT cdc, partitioner
|
|
FROM system_schema.scylla_tables
|
|
WHERE keyspace_name='{test_keyspace}' AND table_name='{get_name(tbl)}'
|
|
""").one()
|
|
new_tbl_scylla_row = cql.execute(f"""
|
|
SELECT cdc, partitioner
|
|
FROM system_schema.scylla_tables
|
|
WHERE keyspace_name='{test_keyspace}' AND table_name='{get_name(new_tbl)}'
|
|
""").one()
|
|
assert tbl_scylla_row == new_tbl_scylla_row
|
|
finally:
|
|
cql.execute(f"DROP TABLE {new_tbl}")
|
|
|
|
# Test that `DESC TYPE {udt}` contains appropriate create statement for user-defined type
|
|
def test_desc_type(cql, test_keyspace, random_seed):
|
|
with new_random_type(cql, test_keyspace) as udt:
|
|
desc = cql.execute(f"DESC TYPE {udt}")
|
|
desc_stmt = desc.one().create_statement
|
|
|
|
new_udt = f"{test_keyspace}.{unique_name()}"
|
|
new_desc_stmt = desc_stmt.replace(udt, new_udt)
|
|
|
|
try:
|
|
cql.execute(new_desc_stmt)
|
|
udt_row = cql.execute(f"""
|
|
SELECT keyspace_name, field_names, field_types
|
|
FROM system_schema.types
|
|
WHERE keyspace_name='{test_keyspace}' AND type_name='{get_name(udt)}'
|
|
""").one()
|
|
new_udt_row = cql.execute(f"""
|
|
SELECT keyspace_name, field_names, field_types
|
|
FROM system_schema.types
|
|
WHERE keyspace_name='{test_keyspace}' AND type_name='{get_name(new_udt)}'
|
|
""").one()
|
|
assert udt_row == new_udt_row
|
|
finally:
|
|
cql.execute(f"DROP TYPE {new_udt}")
|
|
|
|
# Test that `DESC FUNCTION {udf}` contains appropriate create statement for user-defined function
|
|
# The test is `scylla_only` because Scylla's UDF is written in Lua, which is not supported by Cassandra
|
|
def test_desc_function(scylla_only, cql, test_keyspace, random_seed):
|
|
fn_schema = "(val int)"
|
|
fn_null = "RETURNS NULL ON NULL INPUT"
|
|
fn_return = "RETURNS int"
|
|
fn_language = "LANGUAGE lua"
|
|
fn_body = "AS $$return -val$$"
|
|
with new_function(cql, test_keyspace, f"{fn_schema} {fn_null} {fn_return} {fn_language} {fn_body}") as fn:
|
|
desc = cql.execute(f"DESC FUNCTION {test_keyspace}.{fn}")
|
|
desc_stmt = desc.one().create_statement
|
|
|
|
new_fn = unique_name()
|
|
new_desc_stmt = desc_stmt.replace(fn, new_fn)
|
|
|
|
try:
|
|
cql.execute(new_desc_stmt)
|
|
fn_row = cql.execute(f"""
|
|
SELECT argument_types, argument_names, body, called_on_null_input, language, return_type
|
|
FROM system_schema.functions
|
|
WHERE keyspace_name='{test_keyspace}' AND function_name='{fn}'
|
|
""").one()
|
|
new_fn_row = cql.execute(f"""
|
|
SELECT argument_types, argument_names, body, called_on_null_input, language, return_type
|
|
FROM system_schema.functions
|
|
WHERE keyspace_name='{test_keyspace}' AND function_name='{new_fn}'
|
|
""").one()
|
|
|
|
# The columns are tested one by one to be able to trim function's body.
|
|
assert fn_row.argument_types == new_fn_row.argument_types
|
|
assert fn_row.argument_names == new_fn_row.argument_names
|
|
assert fn_row.body.strip() == new_fn_row.body.strip()
|
|
assert fn_row.called_on_null_input == new_fn_row.called_on_null_input
|
|
assert fn_row.language == new_fn_row.language
|
|
assert fn_row.return_type == new_fn_row.return_type
|
|
finally:
|
|
cql.execute(f"DROP FUNCTION {test_keyspace}.{new_fn}")
|
|
|
|
# Test that `DESC AGGREGATE {uda}` contains appropriate create statement for user-defined aggregate
|
|
# The test is `scylla_only` because Scylla's UDA is written in Lua, which is not supported by Cassandra
|
|
def test_desc_aggregate(scylla_only, cql, test_keyspace, random_seed):
|
|
fn_schema = "(val1 int, val2 int)"
|
|
fn_null = "RETURNS NULL ON NULL INPUT"
|
|
fn_return = "RETURNS int"
|
|
fn_language = "LANGUAGE lua"
|
|
fn_body = "return val1 + val2"
|
|
|
|
with new_function(cql, test_keyspace, f"{fn_schema} {fn_null} {fn_return} {fn_language} AS '{fn_body}'") as fn:
|
|
agg_body = f"(int) SFUNC {fn} STYPE int INITCOND 0"
|
|
with new_aggregate(cql, test_keyspace, agg_body) as aggr:
|
|
desc = cql.execute(f"DESC AGGREGATE {test_keyspace}.{aggr}")
|
|
desc_stmt = desc.one().create_statement
|
|
|
|
new_aggr = unique_name()
|
|
new_desc_stmt = desc_stmt.replace(f"{test_keyspace}.{aggr}", f"{test_keyspace}.{new_aggr}")
|
|
|
|
try:
|
|
cql.execute(new_desc_stmt)
|
|
aggr_row = cql.execute(f"""
|
|
SELECT argument_types, final_func, initcond, return_type, state_func, state_type
|
|
FROM system_schema.aggregates
|
|
WHERE keyspace_name='{test_keyspace}' AND aggregate_name='{aggr}'
|
|
""").one()
|
|
new_aggr_row = cql.execute(f"""
|
|
SELECT argument_types, final_func, initcond, return_type, state_func, state_type
|
|
FROM system_schema.aggregates
|
|
WHERE keyspace_name='{test_keyspace}' AND aggregate_name='{new_aggr}'
|
|
""").one()
|
|
assert aggr_row == new_aggr_row
|
|
|
|
aggr_scylla_row = cql.execute(f"""
|
|
SELECT argument_types, reduce_func, state_type
|
|
FROM system_schema.scylla_aggregates
|
|
WHERE keyspace_name='{test_keyspace}' AND aggregate_name='{aggr}'
|
|
""").one()
|
|
new_aggr_scylla_row = cql.execute(f"""
|
|
SELECT argument_types, reduce_func, state_type
|
|
FROM system_schema.scylla_aggregates
|
|
WHERE keyspace_name='{test_keyspace}' AND aggregate_name='{new_aggr}'
|
|
""").one()
|
|
assert aggr_scylla_row == new_aggr_scylla_row
|
|
finally:
|
|
cql.execute(f"DROP AGGREGATE {test_keyspace}.{new_aggr}")
|
|
|
|
# Test that `DESC TABLE {tbl} WITH INTERNALS` contains additional information for added/dropped columns
|
|
def test_desc_table_internals(cql, test_keyspace):
|
|
with new_test_table(cql, test_keyspace, "a int primary key, b int, c int") as tbl:
|
|
cql.execute(f"ALTER TABLE {tbl} DROP b")
|
|
desc = cql.execute(f"DESC TABLE {tbl}").one().create_statement
|
|
desc_internals = cql.execute(f"DESC TABLE {tbl} WITH INTERNALS").one().create_statement
|
|
|
|
assert "b int" not in desc
|
|
assert "b int" in desc_internals
|
|
assert f"ALTER TABLE {tbl} DROP b" in desc_internals
|
|
|
|
cql.execute(f"ALTER TABLE {tbl} ADD b int")
|
|
desc = cql.execute(f"DESC TABLE {tbl}").one().create_statement
|
|
desc_internals = cql.execute(f"DESC TABLE {tbl} WITH INTERNALS").one().create_statement
|
|
|
|
assert "b int" in desc
|
|
assert "b int" in desc_internals
|
|
assert f"ALTER TABLE {tbl} DROP b" in desc_internals
|
|
assert f"ALTER TABLE {tbl} ADD b int" in desc_internals
|
|
|
|
# Test that `DESC KEYSPACE {ks}` contains not only keyspace create statement but also for its elements
|
|
def test_desc_keyspace_elements(cql, random_seed, has_tablets):
|
|
if has_tablets: # issue #18180
|
|
global counter_table_chance
|
|
counter_table_chance = 0
|
|
with new_random_keyspace(cql) as ks:
|
|
with new_random_type(cql, ks) as udt:
|
|
with new_random_table(cql, ks, [udt]) as tbl:
|
|
desc = cql.execute(f"DESC KEYSPACE {ks}")
|
|
ks_desc = " ".join([r.create_statement for r in desc])
|
|
|
|
assert f"CREATE KEYSPACE {ks}" in ks_desc
|
|
assert f"CREATE TYPE {udt}" in ks_desc
|
|
assert f"CREATE TABLE {tbl}" in ks_desc
|
|
|
|
only_desc = cql.execute(f"DESC ONLY KEYSPACE {ks}")
|
|
ks_only_desc = " ".join([r.create_statement for r in only_desc])
|
|
|
|
assert f"CREATE KEYSPACE {ks}" in ks_only_desc
|
|
assert f"CREATE TYPE {udt}" not in ks_only_desc
|
|
assert f"CREATE TABLE {tbl}" not in ks_only_desc
|
|
|
|
# Test that `DESC SCHEMA` contains all information for user created keyspaces
|
|
# and `DESC FULL SCHEMA` contains also information for system keyspaces
|
|
def test_desc_schema(cql, test_keyspace, random_seed, has_tablets):
|
|
if has_tablets: # issue #18180
|
|
global counter_table_chance
|
|
counter_table_chance = 0
|
|
with new_random_keyspace(cql) as ks:
|
|
with new_random_table(cql, test_keyspace) as tbl1, new_random_table(cql, ks) as tbl2:
|
|
desc = cql.execute("DESC SCHEMA")
|
|
schema_desc = " ".join([r.create_statement for r in desc])
|
|
|
|
assert f"CREATE KEYSPACE {test_keyspace}" in schema_desc
|
|
assert f"CREATE TABLE {tbl1}" in schema_desc
|
|
assert f"CREATE KEYSPACE {ks}" in schema_desc
|
|
assert f"CREATE TABLE {tbl2}" in schema_desc
|
|
|
|
full_desc = cql.execute("DESC FULL SCHEMA")
|
|
schema_full_desc = " ".join([r.create_statement for r in full_desc])
|
|
|
|
kss = cql.execute("SELECT keyspace_name FROM system_schema.keyspaces")
|
|
tbls = cql.execute("SELECT keyspace_name, table_name FROM system_schema.tables")
|
|
|
|
for ks_row in kss:
|
|
assert f"CREATE KEYSPACE {maybe_quote(ks_row.keyspace_name)}" in schema_full_desc
|
|
for tbl_row in tbls:
|
|
assert f"CREATE TABLE {maybe_quote(tbl_row.keyspace_name)}.{maybe_quote(tbl_row.table_name)}"
|
|
|
|
# Test that `DESC CLUSTER` contains token ranges to endpoints map
|
|
# The test is `scylla_only` because there is no `system.token_ring` table in Cassandra
|
|
@pytest.mark.parametrize("test_keyspace", ["tablets", "vnodes"], indirect=True)
|
|
def test_desc_cluster(scylla_only, cql, test_keyspace):
|
|
cql.execute(f"USE {test_keyspace}")
|
|
desc = cql.execute("DESC CLUSTER").one()
|
|
|
|
if keyspace_has_tablets(cql, test_keyspace):
|
|
# FIXME: there is no endpoint content for tablet keyspaces yet
|
|
# See https://github.com/scylladb/scylladb/issues/16789
|
|
# When run with tablets, this test currently only validates that `DESC CLUSTER` doesn't fail with tablets.
|
|
return
|
|
|
|
desc_endpoints = []
|
|
for (end_token, endpoints) in desc.range_ownership.items():
|
|
for endpoint in endpoints:
|
|
desc_endpoints.append((end_token, endpoint))
|
|
|
|
result = cql.execute(f"SELECT end_token, endpoint FROM system.token_ring WHERE keyspace_name='{test_keyspace}' ORDER BY table_name, start_token")
|
|
ring_endpoints = [(r.end_token, r.endpoint) for r in result]
|
|
|
|
assert sorted(desc_endpoints) == sorted(ring_endpoints)
|
|
|
|
def is_scylla(cql):
|
|
return any('scylla' in name for name in [row.table_name for row in cql.execute("SELECT * FROM system_schema.tables WHERE keyspace_name = 'system'")])
|
|
|
|
# Test that 'DESC INDEX' contains create statement od index
|
|
def test_desc_index(cql, test_keyspace):
|
|
with new_test_table(cql, test_keyspace, "a int primary key, b int, c int") as tbl:
|
|
tbl_name = get_name(tbl)
|
|
create_idx_b = f"CREATE INDEX ON {tbl}(b)"
|
|
create_idx_c = f"CREATE INDEX named_index ON {tbl}(c)"
|
|
# Only Scylla supports local indexes
|
|
has_local = is_scylla(cql)
|
|
if has_local:
|
|
create_idx_ab = f"CREATE INDEX ON {tbl}((a), b)"
|
|
|
|
cql.execute(create_idx_b)
|
|
cql.execute(create_idx_c)
|
|
if has_local:
|
|
cql.execute(create_idx_ab)
|
|
|
|
b_desc = cql.execute(f"DESC INDEX {test_keyspace}.{tbl_name}_b_idx").one().create_statement
|
|
if has_local:
|
|
ab_desc = cql.execute(f"DESC INDEX {test_keyspace}.{tbl_name}_b_idx_1").one().create_statement
|
|
c_desc = cql.execute(f"DESC INDEX {test_keyspace}.named_index").one().create_statement
|
|
|
|
# Cassandra inserts a space between the table name and parentheses,
|
|
# Scylla doesn't. This difference doesn't matter because both are
|
|
# valid CQL commands
|
|
if is_scylla(cql):
|
|
maybe_space = ''
|
|
else:
|
|
maybe_space = ' '
|
|
assert f"CREATE INDEX {tbl_name}_b_idx ON {tbl}{maybe_space}(b)" in b_desc
|
|
if has_local:
|
|
assert f"CREATE INDEX {tbl_name}_b_idx_1 ON {tbl}((a), b)" in ab_desc
|
|
|
|
assert f"CREATE INDEX named_index ON {tbl}{maybe_space}(c)" in c_desc
|
|
|
|
def test_desc_index_on_collections(cql, test_keyspace):
|
|
# In this test, all assertions are in form of
|
|
# `assert create_stmt in desc or create_stmt.replace("(", " (", 1) in desc`
|
|
# This is because Scylla and Cassandra have slightly different (whitespace-wise)
|
|
# output of `DESC INDEX`.
|
|
# Scylla: `CREATE INDEX ON table_name(column_name)`
|
|
# Cassandra: `CREATE INDEX ON table_name (column_name)`
|
|
#
|
|
# This helper function asserts index create statement is present in the description
|
|
# and it matches both Scylla and Cassandra description output style.
|
|
def assert_desc_index(create_stmt, description):
|
|
cassandra_format_create = create_stmt.replace("(", " (", 1)
|
|
assert create_stmt in description or cassandra_format_create in description
|
|
|
|
|
|
with new_test_table(cql, test_keyspace, "a int primary key, b int, c frozen<set<int>>, d map<int, int>, e list<int>, f set<int>, g list<int>, h set<int>, akeysb set<int>") as tbl:
|
|
indexes = [
|
|
("idx_b", f"CREATE INDEX idx_b ON {tbl}(b)"),
|
|
("idx_c", f"CREATE INDEX idx_c ON {tbl}(full(c))"),
|
|
("idx_d_entries", f"CREATE INDEX idx_d_entries ON {tbl}(entries(d))"),
|
|
("idx_d_keys", f"CREATE INDEX idx_d_keys ON {tbl}(keys(d))"),
|
|
("idx_d_values", f"CREATE INDEX idx_d_values ON {tbl}(values(d))"),
|
|
("idx_e", f"CREATE INDEX idx_e ON {tbl}(values(e))"),
|
|
("idx_f", f"CREATE INDEX idx_f ON {tbl}(values(f))"),
|
|
("idx_akeysb", f"CREATE INDEX idx_akeysb ON {tbl}(values(akeysb))"),
|
|
]
|
|
|
|
for name, create_stmt in indexes:
|
|
cql.execute(create_stmt)
|
|
desc = cql.execute(f"DESC INDEX {test_keyspace}.{name}").one().create_statement
|
|
assert_desc_index(create_stmt, desc)
|
|
|
|
# When an index is created on list column with `CREATE INDEX ON tbl(list_column)`,
|
|
# its target is `values(list_column)` and describe will print create statement with that
|
|
# values() function added.
|
|
cql.execute(f"CREATE INDEX idx_g ON {tbl}(g)")
|
|
idx_g_desc = cql.execute(f"DESC INDEX {test_keyspace}.idx_g").one().create_statement
|
|
idx_g_create_stmt = f"CREATE INDEX idx_g ON {tbl}(values(g))"
|
|
assert_desc_index(idx_g_create_stmt, idx_g_desc)
|
|
|
|
# The same situation but with set
|
|
cql.execute(f"CREATE INDEX idx_h ON {tbl}(h)")
|
|
idx_h_desc = cql.execute(f"DESC INDEX {test_keyspace}.idx_h").one().create_statement
|
|
idx_h_create_stmt = f"CREATE INDEX idx_h ON {tbl}(values(h))"
|
|
assert_desc_index(idx_h_create_stmt, idx_h_desc)
|
|
|
|
with pytest.raises(InvalidRequest):
|
|
cql.execute(f"DESC INDEX {test_keyspace}.nosuchindex")
|
|
with pytest.raises(InvalidRequest):
|
|
cql.execute(f"DESC INDEX {tbl}")
|
|
|
|
def test_desc_index_column_quoting(cql, test_keyspace):
|
|
# In this test, all assertions are in form of
|
|
# `assert create_stmt in desc or create_stmt.replace("(", " (", 1)`
|
|
# This is because Scylla and Cassandra have slightly different (whitespace-wise)
|
|
# output of `DESC INDEX`.
|
|
# Scylla: `CREATE INDEX ON table_name(column_name)`
|
|
# Cassandra: `CREATE INDEX ON table_name (column_name)`
|
|
|
|
quoted_names = ['"select"', '"hEllo"', '"x y"', '"hi""hello""yo"', '"""hi"""']
|
|
with new_test_table(cql, test_keyspace, "a int primary key, \"keys(m)\" map<int, int>, \"values(m)\" set<int>, \"entries(m)\" map<int, int>, " + ', '.join([name + " int" for name in quoted_names])) as tbl:
|
|
indexes = [
|
|
("idx_1", f"CREATE INDEX idx_1 ON {tbl}(keys(\"keys(m)\"))"),
|
|
("idx_2", f"CREATE INDEX idx_2 ON {tbl}(values(\"values(m)\"))"),
|
|
("idx_3", f"CREATE INDEX idx_3 ON {tbl}(entries(\"entries(m)\"))"),
|
|
] + [(f"idx_{n}", f"CREATE INDEX idx_{n} ON {tbl}({name})") for n, name in enumerate(quoted_names, 4)]
|
|
|
|
for name, create_stmt in indexes:
|
|
cql.execute(create_stmt)
|
|
desc = cql.execute(f"DESC INDEX {test_keyspace}.{name}").one().create_statement
|
|
assert create_stmt in desc or create_stmt.replace("(", " (", 1)
|
|
|
|
def test_desc_local_secondary_index(scylla_only, cql, test_keyspace):
|
|
with new_test_table(cql, test_keyspace, "a int primary key, b int, c frozen<list<int>>") as tbl:
|
|
indexes = [
|
|
("idx_1", f"CREATE INDEX idx_1 ON {tbl}((a), b)"),
|
|
("idx_2", f"CREATE INDEX idx_2 ON {tbl}((a), full(c))"),
|
|
]
|
|
|
|
for name, create_stmt in indexes:
|
|
cql.execute(create_stmt)
|
|
desc = cql.execute(f"DESC INDEX {test_keyspace}.{name}").one().create_statement
|
|
assert create_stmt in desc
|
|
|
|
# Test that 'DESC TABLE' contains description of indexes
|
|
def test_index_desc_in_table_desc(cql, test_keyspace):
|
|
with new_test_table(cql, test_keyspace, "a int primary key, b int, c int") as tbl:
|
|
create_idx_b = f"CREATE INDEX ON {tbl}(b)"
|
|
create_idx_c = f"CREATE INDEX named_index ON {tbl}(c)"
|
|
has_local = is_scylla(cql)
|
|
if is_scylla(cql):
|
|
maybe_space = ''
|
|
else:
|
|
maybe_space = ' '
|
|
if has_local:
|
|
create_idx_ab = f"CREATE INDEX ON {tbl}((a), b)"
|
|
|
|
cql.execute(create_idx_b)
|
|
cql.execute(create_idx_c)
|
|
if has_local:
|
|
cql.execute(create_idx_ab)
|
|
|
|
tbl_name = get_name(tbl)
|
|
desc = "\n".join([d.create_statement for d in cql.execute(f"DESC TABLE {tbl}")])
|
|
|
|
assert f"CREATE INDEX {tbl_name}_b_idx ON {tbl}{maybe_space}(b)" in desc
|
|
assert f"CREATE INDEX named_index ON {tbl}{maybe_space}(c)" in desc
|
|
if has_local:
|
|
assert f"CREATE INDEX {tbl_name}_b_idx_1 ON {tbl}((a), b)" in desc
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# "Generic describe" is a describe statement without specifying what kind of object
|
|
# you want to describe, so it only requires keyspace name(optionally) and object name.
|
|
# Generic describe should be exactly the same as normal describe, so for instannce:
|
|
# `DESC TABLE <table_name> == DESC <table_name>`.
|
|
#
|
|
# ScyllaDB looks for describing object in a following order:
|
|
# keyspace, table, view, index, UDT, UDF, UDA
|
|
|
|
# Cassandra compatibility require us to be able generic describe: keyspace, table, view, index.
|
|
def test_generic_desc(cql, random_seed, has_tablets):
|
|
if has_tablets: # issue #18180
|
|
global counter_table_chance
|
|
counter_table_chance = 0
|
|
with new_random_keyspace(cql) as ks:
|
|
with new_random_table(cql, ks) as t1, new_test_table(cql, ks, "a int primary key, b int, c int") as tbl:
|
|
cql.execute(f"CREATE INDEX idx ON {tbl}(b)")
|
|
|
|
generic_ks = cql.execute(f"DESC {ks}")
|
|
generic_t1 = cql.execute(f"DESC {t1}")
|
|
generic_tbl = cql.execute(f"DESC {tbl}")
|
|
generic_idx = cql.execute(f"DESC {ks}.idx")
|
|
|
|
desc_ks = cql.execute(f"DESC KEYSPACE {ks}")
|
|
desc_t1 = cql.execute(f"DESC TABLE {t1}")
|
|
desc_tbl = cql.execute(f"DESC TABLE {tbl}")
|
|
desc_idx = cql.execute(f"DESC INDEX {ks}.idx")
|
|
|
|
assert generic_ks == desc_ks
|
|
assert generic_t1 == desc_t1
|
|
assert generic_tbl == desc_tbl
|
|
assert generic_idx == desc_idx
|
|
|
|
# We've extended generic describe to include user-defined objects: UDTs, UDFs and UDAs.
|
|
# Since Cassandra doesn't generic description of support user-defined objects,
|
|
# the test is `scylla_only`.
|
|
# Reproduces #14170
|
|
def test_generic_desc_user_defined(scylla_only, cql, test_keyspace):
|
|
with new_random_type(cql, test_keyspace) as udt:
|
|
assert cql.execute(f"DESC {udt}") == cql.execute(f"DESC TYPE {udt}")
|
|
|
|
with new_function(cql, test_keyspace, """
|
|
(val1 int, val2 int)
|
|
RETURNS NULL ON NULL INPUT
|
|
RETURNS int
|
|
LANGUAGE lua
|
|
AS 'return val1 + val2'
|
|
""") as fn:
|
|
assert cql.execute(f"DESC {test_keyspace}.{fn}") == cql.execute(f"DESC FUNCTION {test_keyspace}.{fn}")
|
|
|
|
with new_aggregate(cql, test_keyspace, f"(int) SFUNC {fn} STYPE int INITCOND 0") as aggr:
|
|
assert cql.execute(f"DESC {test_keyspace}.{aggr}") == cql.execute(f"DESC AGGREGATE {test_keyspace}.{aggr}")
|
|
|
|
|
|
# Test that 'DESC FUNCTION'/'DESC AGGREGATE' doesn't show UDA/UDF and doesn't crash Scylla
|
|
# The test is marked scylla_only because it uses Lua as UDF language.
|
|
def test_desc_udf_uda(cql, test_keyspace, scylla_only):
|
|
with new_function(cql, test_keyspace, "(a int, b int) RETURNS NULL ON NULL INPUT RETURNS int LANGUAGE LUA AS 'return a+b'") as fn:
|
|
with new_aggregate(cql, test_keyspace, f"(int) SFUNC {fn} STYPE int") as aggr:
|
|
|
|
with pytest.raises(InvalidRequest):
|
|
cql.execute(f"DESC FUNCTION {test_keyspace}.{aggr}")
|
|
with pytest.raises(InvalidRequest):
|
|
cql.execute(f"DESC AGGREGATE {test_keyspace}.{fn}")
|
|
|
|
# Test that JSON objects in DESC TABLE contains whitespaces.
|
|
# In the old client-side describe there was a whitespace after
|
|
# each colon(:) and comma(,).
|
|
# The new server-side describe was missing it.
|
|
# Example: caching = {'keys': 'ALL', 'rows_per_partition': 'ALL'}
|
|
# Reproduces #14895
|
|
# The test is marked scylla_only because it uses a Scylla-only property "cdc".
|
|
@pytest.mark.parametrize("test_keyspace",
|
|
[pytest.param("tablets", marks=[pytest.mark.xfail(reason="issue #16317")]), "vnodes"],
|
|
indirect=True)
|
|
def test_whitespaces_in_table_options(cql, test_keyspace, scylla_only):
|
|
regex = "\\{[^}]*[:,][^\\s][^}]*\\}" # looks for any colon or comma without space after it inside a { }
|
|
|
|
with new_test_table(cql, test_keyspace, "a int primary key", "WITH cdc = {'enabled': true}") as tbl:
|
|
desc = "\n".join([d.create_statement for d in cql.execute(f"DESC TABLE {tbl}")])
|
|
assert re.search(regex, desc) == None
|
|
|
|
# Randomly create many UDTs, with dependencies between them
|
|
# and validate if describe displays them in correct order.
|
|
# UDTs should be sorted topologically, meaning if UDT `a`
|
|
# is used to create UDT `b`, then `a` should be before `b`.
|
|
def test_udt_sorting(scylla_only, cql, test_keyspace, random_seed):
|
|
repeat = 10 # test it randomized, so repeat it inside the test to increase coverage
|
|
max_selected_udts = 5 # max number of UDTs that can be used to create a new UDT
|
|
|
|
# Keyspace for recreating UDTs
|
|
with new_test_keyspace(cql, "WITH REPLICATION = {'class': 'NetworkTopologyStrategy', 'replication_factor': 1}") as keyspace:
|
|
for _ in range(repeat):
|
|
created_udts = []
|
|
udts_cnt = 10
|
|
udt_backup = []
|
|
|
|
with ExitStack() as stack:
|
|
# Create `udts_cnt` many UDTs
|
|
while len(created_udts) < udts_cnt:
|
|
select_cnt = random.randint(0, min(max_selected_udts, len(created_udts))) # how many UDTs we want to use to create a new one
|
|
udt_body = "(a int)"
|
|
if select_cnt != 0:
|
|
selected_udts = random.sample(created_udts, select_cnt) # randomly select `select_cnt` already created UDTs
|
|
udt_body = create_udt_body(selected_udts)
|
|
|
|
udt = stack.enter_context(new_type(cql, test_keyspace, udt_body))
|
|
created_udts.append(udt)
|
|
|
|
# Describe all UDTs, iterate over all of them and verify the order is correct.
|
|
# (If UDT `u` is used create UDT `w`, `u` has to be described before `w`)
|
|
desc = cql.execute(f"DESC KEYSPACE {test_keyspace}").all()
|
|
visited_udts = set()
|
|
for row in desc:
|
|
if row.type == "type":
|
|
# Extract UDTs used in current UDT
|
|
# Names of all UDTs begin with `unique_name_prefix`
|
|
for it in re.finditer(f"<({unique_name_prefix}[^\s\>]+)>", row.create_statement):
|
|
assert f"{test_keyspace}.{it.group(1)}" in visited_udts
|
|
visited_udts.add(f"{row.keyspace_name}.{row.name}")
|
|
|
|
assert visited_udts == set(created_udts)
|
|
udt_backup = desc
|
|
|
|
with ExitStack() as stack:
|
|
drop_type = lambda cql, keyspace_name, type_name: cql.execute(f"DROP TYPE {keyspace_name}.{type_name}")
|
|
|
|
# Recreate all UDTs from the description in a designated keyspace.
|
|
for row in desc:
|
|
if row.type == "type":
|
|
cql.execute(row.create_statement.replace(test_keyspace, keyspace))
|
|
stack.callback(drop_type, cql=cql, keyspace_name=keyspace, type_name=row.name)
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Following tests `test_*_quoting` check if names inside elements' descriptions are quoted if needed.
|
|
# The tests don't check if create statements are correct, but only assert if the name inside is quoted.
|
|
# Names which are testes are extracted to separate variable at the beginning of each test.
|
|
|
|
def test_keyspaces_quoting(cql):
|
|
# Keyspace name must match [a-zA-Z0-9_]+, so the only names which require quoting
|
|
# are the ones with capital letters.
|
|
name = "Quoted_KS"
|
|
|
|
cql.execute(f"CREATE KEYSPACE \"{name}\" WITH REPLICATION = {{'class': 'NetworkTopologyStrategy', 'replication_factor': 1}}")
|
|
try:
|
|
desc = cql.execute(f"DESC KEYSPACE \"{name}\"").one().create_statement
|
|
|
|
# Check if name in create statement is quoted
|
|
assert f"CREATE KEYSPACE \"{name}\" WITH" in desc
|
|
assert f"CREATE KEYSPACE {name} WITH" not in desc
|
|
|
|
# Check if name if keyspaces listing is quoted
|
|
with new_cql(cql) as ncql:
|
|
keyspaces_list = [(r.keyspace_name, r.name) for r in ncql.execute("DESC KEYSPACES")]
|
|
|
|
assert (f"\"{name}\"", f"\"{name}\"") in keyspaces_list
|
|
assert (f"\"{name}\"", name) not in keyspaces_list
|
|
assert (name, f"\"{name}\"") not in keyspaces_list
|
|
finally:
|
|
cql.execute(f"DROP KEYSPACE \"{name}\"")
|
|
|
|
def test_table_quoting(cql):
|
|
# Table name must match [a-zA-Z0-9_]+, so the only names which require quoting
|
|
# are the ones with capital letters.
|
|
ks_name = "Quoted_KS"
|
|
name = "Quoted_TABLE"
|
|
col_name = "!@#$%"
|
|
|
|
cql.execute(f"CREATE KEYSPACE \"{ks_name}\" WITH REPLICATION = {{'class': 'NetworkTopologyStrategy', 'replication_factor': 1}}")
|
|
try:
|
|
cql.execute(f"CREATE TABLE \"{ks_name}\".\"{name}\"(a int primary key, \"{col_name}\" int)")
|
|
desc = cql.execute(f"DESC TABLE \"{ks_name}\".\"{name}\"").one().create_statement
|
|
|
|
assert f"CREATE TABLE \"{ks_name}\".\"{name}\"" in desc
|
|
assert f"CREATE TABLE {ks_name}.\"{name}\"" not in desc
|
|
assert f"CREATE TABLE \"{ks_name}\".{name}" not in desc
|
|
|
|
assert f"\"{col_name}\" int" in desc
|
|
assert f"{col_name} int" not in desc
|
|
|
|
cql.execute(f"ALTER TABLE \"{ks_name}\".\"{name}\" DROP \"!@#$%\"")
|
|
cql.execute(f"ALTER TABLE \"{ks_name}\".\"{name}\" ADD \"!@#$%\" int")
|
|
desc_alter = cql.execute(f"DESC TABLE \"{ks_name}\".\"{name}\" WITH INTERNALS").one().create_statement
|
|
|
|
assert f"ALTER TABLE \"{ks_name}\".\"{name}\" DROP \"!@#$%\"" in desc_alter
|
|
assert f"ALTER TABLE \"{ks_name}\".\"{name}\" ADD \"!@#$%\"" in desc_alter
|
|
|
|
with new_cql(cql) as ncql:
|
|
tables_list = [(r.keyspace_name, r.name) for r in ncql.execute("DESC TABLES")]
|
|
|
|
assert (f"\"{ks_name}\"", f"\"{name}\"") in tables_list
|
|
assert (f"\"{ks_name}\"", name) not in tables_list
|
|
assert (ks_name, f"\"{name}\"") not in tables_list
|
|
finally:
|
|
cql.execute(f"DROP TABLE \"{ks_name}\".\"{name}\"")
|
|
cql.execute(f"DROP KEYSPACE \"{ks_name}\"")
|
|
|
|
def test_type_quoting(cql):
|
|
ks_name = "Quoted_KS"
|
|
name = "udt_@@@"
|
|
field_name = "field_!!!"
|
|
|
|
cql.execute(f"CREATE KEYSPACE \"{ks_name}\" WITH REPLICATION = {{'class': 'NetworkTopologyStrategy', 'replication_factor': 1}}")
|
|
try:
|
|
cql.execute(f"CREATE TYPE \"{ks_name}\".\"{name}\" (a int, \"{field_name}\" text)")
|
|
desc = cql.execute(f"DESC TYPE \"{ks_name}\".\"{name}\"").one().create_statement
|
|
|
|
assert f"CREATE TYPE \"{ks_name}\".\"{name}\"" in desc
|
|
assert f"CREATE TYPE \"{ks_name}\".{name}" not in desc
|
|
assert f"CREATE TYPE {ks_name}.\"{name}\"" not in desc
|
|
# The following used to be buggy in Cassandra, but fixed in
|
|
# CASSANDRA-17918 (in Cassandra 4.1.2):
|
|
assert f"\"{field_name}\" text" in desc
|
|
assert f"{field_name} text" not in desc
|
|
|
|
with new_cql(cql) as ncql:
|
|
types_list = [(r.keyspace_name, r.name) for r in ncql.execute("DESC TYPES")]
|
|
|
|
assert (f"\"{ks_name}\"", f"\"{name}\"") in types_list
|
|
assert (f"\"{ks_name}\"", name) not in types_list
|
|
assert (ks_name, f"\"{name}\"") not in types_list
|
|
finally:
|
|
cql.execute(f"DROP TYPE \"{ks_name}\".\"{name}\"")
|
|
cql.execute(f"DROP KEYSPACE \"{ks_name}\"")
|
|
|
|
def test_function_quoting(scylla_only, cql, test_keyspace):
|
|
ks_name = "Quoted_KS"
|
|
name = "!udf!"
|
|
|
|
cql.execute(f"CREATE KEYSPACE \"{ks_name}\" WITH REPLICATION = {{'class': 'NetworkTopologyStrategy', 'replication_factor': 1}}")
|
|
try:
|
|
cql.execute(f"""
|
|
CREATE FUNCTION \"{ks_name}\".\"{name}\"(val int)
|
|
RETURNS NULL ON NULL INPUT
|
|
RETURNS int
|
|
LANGUAGE lua
|
|
AS $$ return val $$
|
|
""")
|
|
desc = cql.execute(f"DESC FUNCTION \"{ks_name}\".\"{name}\"").one().create_statement
|
|
|
|
assert f"CREATE FUNCTION \"{ks_name}\".\"{name}\"" in desc
|
|
assert f"CREATE FUNCTION \"{ks_name}\".{name}" not in desc
|
|
assert f"CREATE FUNCTION {ks_name}.\"{name}\"" not in desc
|
|
|
|
with new_cql(cql) as ncql:
|
|
udf_list = [(r.keyspace_name, r.name) for r in ncql.execute("DESC FUNCTIONS")]
|
|
|
|
assert (f"\"{ks_name}\"", f"\"{name}\"") in udf_list
|
|
assert (f"\"{ks_name}\"", name) not in udf_list
|
|
assert (ks_name, f"\"{name}\"") not in udf_list
|
|
finally:
|
|
cql.execute(f"DROP FUNCTION \"{ks_name}\".\"{name}\"")
|
|
cql.execute(f"DROP KEYSPACE \"{ks_name}\"")
|
|
|
|
def test_aggregate_quoting(scylla_only, cql, test_keyspace):
|
|
ks_name = "Quoted_KS"
|
|
sfunc_name = "!udf!"
|
|
name = "'uda'!"
|
|
|
|
cql.execute(f"CREATE KEYSPACE \"{ks_name}\" WITH REPLICATION = {{'class': 'NetworkTopologyStrategy', 'replication_factor': 1}}")
|
|
try:
|
|
cql.execute(f"""
|
|
CREATE FUNCTION \"{ks_name}\".\"{sfunc_name}\"(val1 int, val2 int)
|
|
RETURNS NULL ON NULL INPUT
|
|
RETURNS int
|
|
LANGUAGE lua
|
|
AS $$ return val1 + val2 $$
|
|
""")
|
|
cql.execute(f"""
|
|
CREATE AGGREGATE \"{ks_name}\".\"{name}\"(int)
|
|
SFUNC \"{sfunc_name}\"
|
|
STYPE int
|
|
""")
|
|
desc = cql.execute(f"DESC AGGREGATE \"{ks_name}\".\"{name}\"").one().create_statement
|
|
|
|
assert f"CREATE AGGREGATE \"{ks_name}\".\"{name}\"" in desc
|
|
assert f"SFUNC \"{sfunc_name}\"" in desc
|
|
|
|
with new_cql(cql) as ncql:
|
|
uda_list = [(r.keyspace_name, r.name) for r in ncql.execute("DESC AGGREGATES")]
|
|
|
|
assert (f"\"{ks_name}\"", f"\"{name}\"") in uda_list
|
|
assert (f"\"{ks_name}\"", name) not in uda_list
|
|
assert (ks_name, f"\"{name}\"") not in uda_list
|
|
finally:
|
|
cql.execute(f"DROP AGGREGATE \"{ks_name}\".\"{name}\"")
|
|
cql.execute(f"DROP FUNCTION \"{ks_name}\".\"{sfunc_name}\"")
|
|
cql.execute(f"DROP KEYSPACE \"{ks_name}\"")
|
|
|
|
# Test if fields and options (column names, column types, comment) inside table's description are quoted properly
|
|
def test_table_options_quoting(cql, test_keyspace):
|
|
type_name = f"some_udt; DROP KEYSPACE {test_keyspace}"
|
|
column_name = "col''umn -- @quoting test!!"
|
|
comment = "table''s comment test!\"; DESC TABLES --quoting test"
|
|
comment_plain = "table's comment test!\"; DESC TABLES --quoting test" #without doubling "'" inside comment
|
|
|
|
cql.execute(f"CREATE TYPE {test_keyspace}.\"{type_name}\" (a int)")
|
|
try:
|
|
with new_test_table(cql, test_keyspace, f"a int primary key, b list<frozen<\"{type_name}\">>, \"{column_name}\" int",
|
|
f"with comment = '{comment}'") as tbl:
|
|
desc = cql.execute(f"DESC TABLE {tbl}").one().create_statement
|
|
|
|
assert f"b list<frozen<\"{type_name}\">>" in desc
|
|
assert f"\"{column_name}\" int" in desc
|
|
assert f"comment = '{comment}'" in desc
|
|
|
|
assert f"b list<frozen<{type_name}>>" not in desc
|
|
assert f"{column_name} int" not in desc
|
|
assert f"comment = \"{comment}\"" not in desc
|
|
assert f"comment = {comment}" not in desc
|
|
assert f"comment = '{comment_plain}'" not in desc
|
|
finally:
|
|
cql.execute(f"DROP TYPE {test_keyspace}.\"{type_name}\"")
|
|
|
|
# We need to hide cdc log tables (more precisely, their CREATE statemtns and/or names)
|
|
# but we are attaching `ALTER TABLE <cdc log table name> WITH <all table's properties>` to description of base table
|
|
@pytest.mark.parametrize("test_keyspace",
|
|
[pytest.param("tablets", marks=[pytest.mark.xfail(reason="issue #16317")]), "vnodes"],
|
|
indirect=True)
|
|
def test_hide_cdc_table(scylla_only, cql, test_keyspace):
|
|
cdc_table_suffix = "_scylla_cdc_log"
|
|
|
|
with new_test_table(cql, test_keyspace, "a int primary key, b int", "WITH cdc = {'enabled': true}") as t:
|
|
t_name = t.split('.')[1]
|
|
cdc_log_name = t_name + cdc_table_suffix
|
|
|
|
# Check if the log table exists
|
|
cdc_log_table_entry = cql.execute(f"SELECT * FROM system_schema.tables WHERE keyspace_name='{test_keyspace}' AND table_name='{cdc_log_name}'").all()
|
|
assert len(cdc_log_table_entry) == 1
|
|
|
|
# DESC TABLES
|
|
desc_tables = cql.execute("DESC TABLES")
|
|
assert cdc_log_name not in [r.name for r in desc_tables]
|
|
|
|
# DESC KEYSPACE ks
|
|
desc_keyspace = cql.execute(f"DESC KEYSPACE {test_keyspace}").all()
|
|
for row in desc_keyspace:
|
|
if row.name == cdc_log_name:
|
|
assert f"ALTER TABLE {test_keyspace}.{cdc_log_name} WITH" in row.create_statement
|
|
|
|
# DESC SCHEMA
|
|
desc_schema = cql.execute("DESC SCHEMA")
|
|
for row in desc_schema:
|
|
if row.name == cdc_log_name:
|
|
assert f"ALTER TABLE {test_keyspace}.{cdc_log_name} WITH" in row.create_statement
|
|
|
|
# Check 't_scylla_cdc_log' cannot be described directly
|
|
with pytest.raises(InvalidRequest, match=f"{test_keyspace}.{cdc_log_name} is a cdc log table and it cannot be described directly. Try `DESC TABLE {test_keyspace}.{t_name}` to describe cdc base table and it's log table."):
|
|
desc_cdc_table = cql.execute(f"DESC TABLE {test_keyspace}.{cdc_log_name}")
|
|
|
|
# Check base table description contains ALTER TABLE statement for cdc log table
|
|
desc_base_table = cql.execute(f"DESC TABLE {t}").all()
|
|
assert f"ALTER TABLE {test_keyspace}.{cdc_log_name} WITH" in desc_base_table[1].create_statement
|
|
|
|
# Drop current cdc base table and try to recreate it with describe output
|
|
cql.execute(f"DROP TABLE {t}")
|
|
for row in desc_keyspace[1:]: # [1:] because we want to skip first row (keyspace's CREATE STATEMENT)
|
|
cql.execute(row.create_statement)
|
|
|
|
# Check if base and log tables were recreated
|
|
ks_tables = cql.execute(f"SELECT * FROM system_schema.tables WHERE keyspace_name='{test_keyspace}'").all()
|
|
assert len(ks_tables) == 2
|
|
for row in ks_tables:
|
|
assert row.table_name == t_name or row.table_name == cdc_log_name
|
|
|
|
|
|
### =========================== UTILITY FUNCTIONS =============================
|
|
|
|
# Create random body of UDT using all UDTs from `udts`.
|
|
# The functions tries to create complex field types,
|
|
# by mixing lists, tuples, sets, etc...
|
|
def create_udt_body(udts):
|
|
two_udts_type_probability = 0.3
|
|
nest_probability = 0.1
|
|
|
|
one_udt_type = lambda u: random.choice([
|
|
f"frozen<{u}>",
|
|
f"frozen<list<{u}>>",
|
|
f"list<frozen<{u}>>",
|
|
f"frozen<set<{u}>>",
|
|
f"set<frozen<{u}>>",
|
|
f"tuple<frozen<{u}>, int>",
|
|
f"frozen<tuple<{u}, int>>",
|
|
f"map<frozen<{u}>, int>",
|
|
f"map<int, frozen<{u}>>",
|
|
f"frozen<map<{u}, int>>",
|
|
f"frozen<map<int, {u}>>",
|
|
])
|
|
two_udts_type = lambda u, w: random.choice([
|
|
f"frozen<tuple<{u}, {w}>>",
|
|
f"tuple<frozen<{u}>, frozen<{w}>>",
|
|
f"frozen<map<{u}, {w}>>",
|
|
f"map<frozen<{u}>, frozen<{w}>>"
|
|
])
|
|
|
|
fields = []
|
|
field_idx = 1
|
|
while udts:
|
|
# Select to create one_udt_type or two_udts_type
|
|
two_udts = False
|
|
if len(udts) > 1 and random.random() < two_udts_type_probability:
|
|
two_udts = True
|
|
|
|
field_type = None
|
|
if two_udts: # Create a type based on two UDTs (two_udts_type)
|
|
u1 = udts.pop()
|
|
u2 = udts.pop()
|
|
field_type = two_udts_type(u1, u2)
|
|
else: # Create a type based on one UDT (one_udt_type)
|
|
field_type = one_udt_type(udts.pop())
|
|
|
|
# Maybe create nested type
|
|
if random.random() < nest_probability:
|
|
field_type = one_udt_type(field_type)
|
|
|
|
fields.append(f"f_{field_idx} {field_type}")
|
|
field_idx = field_idx + 1
|
|
|
|
fields_joined = ", ".join(fields)
|
|
return f"({fields_joined})"
|
|
|
|
def get_name(name_with_ks):
|
|
return name_with_ks.split(".")[1]
|
|
|
|
# Takes an identifier and quotes it when needed.
|
|
# This is a copy of `maybe_quote` from cql3's utils.
|
|
# (see 'cql3/cql3_type.cc:430')
|
|
# NOTE: This function is missing a functionality of running cql's grammar function
|
|
def maybe_quote(ident):
|
|
if not len(ident):
|
|
return "\"\""
|
|
|
|
need_quotes = False
|
|
num_quotes = 0
|
|
for c in ident:
|
|
if not (('a' <= c and c <= 'z') or ('0' <= c and c <= '9') or c =='_'):
|
|
need_quotes = True
|
|
num_quotes += (c == '"')
|
|
|
|
if not need_quotes:
|
|
# TODO: Here is missing part from C++ implementation (see cql3/cql3_type.cc:448)
|
|
# Here function from Cql.g (cident) is used to ensure the ident doesn't need quoting.
|
|
return ident
|
|
|
|
if num_quotes == 0:
|
|
return '"' + ident + '"'
|
|
|
|
return '"' + ident.replace('"', "\"\"") + '"'
|
|
|
|
### ---------------------------------------------------------------------------
|
|
|
|
# Configuration parameters for randomly created elements (keyspaces/tables/types)
|
|
|
|
# If depth reaches 0, random type will return native type
|
|
max_depth = 2
|
|
# How many types can be in collection/tuple/UDT
|
|
max_types = 5
|
|
# How many rows can be in partition key
|
|
max_pk = 3
|
|
# How many rows can be in clustering key
|
|
max_ck = 5
|
|
# How many regular rows can be in table
|
|
max_regular = 4
|
|
# How many static rows can be in table
|
|
max_static = 3
|
|
# Chance of counter table instead of normal one
|
|
counter_table_chance = 0.1
|
|
|
|
# A utility function for obtaining random native type
|
|
def get_random_native_type():
|
|
types = ["text", "bigint", "boolean", "decimal", "double", "float", "int"]
|
|
return random.choice(types)
|
|
|
|
# A utility function for obtaining random collection type.
|
|
# `udts` argument represents user-defined types that can be used for
|
|
# creating the collection type. If `frozen` is true, then the function will
|
|
# return `frozen<collection_type>`
|
|
def get_random_collection_type(cql, keyspace, depth, udts=[], frozen=False):
|
|
types = ["set", "list", "map"]
|
|
collection_type = random.choice(types)
|
|
|
|
if collection_type == "set":
|
|
return_type = f"set<{get_random_type(cql, keyspace, depth-1, udts, True)}>"
|
|
elif collection_type == "list":
|
|
return_type = f"list<{get_random_type(cql, keyspace, depth-1, udts, True)}>"
|
|
elif collection_type == "map":
|
|
return_type = f"map<{get_random_type(cql, keyspace, depth-1, udts, True)}, {get_random_type(cql, keyspace, depth-1, udts, True)}>"
|
|
return f"frozen<{return_type}>" if frozen else return_type
|
|
|
|
# A utility function for obtaining random tuple type.
|
|
# `udts` argument represents user-defined types that can be used for
|
|
# creating the tuple type. If `frozen` is true, then the function will
|
|
# return `frozen<tuple<some_fields>>`
|
|
def get_random_tuple_type(cql, keyspace, depth, udts=[], frozen=False):
|
|
n = random.randrange(1, max_types+1)
|
|
types = [get_random_type(cql, keyspace, depth-1, udts, False) for _ in range(n)]
|
|
fields = ", ".join(types)
|
|
return_type = f"tuple<{fields}>"
|
|
|
|
return f"frozen<{return_type}>" if frozen else return_type
|
|
|
|
# A utility function for obtaining random type. The function can return
|
|
# native types, collections, tuples and UDTs. `udts` argument represents
|
|
# user-defined types that can be returned or used for creation of other types
|
|
# If `frozen` is true, then the function will return `frozen<some_type>`
|
|
def get_random_type(cql, keyspace, depth, udts=[], frozen=False):
|
|
types = ["native", "collection", "tuple"]
|
|
if len(udts) != 0:
|
|
types.append("UDT")
|
|
type_type = random.choice(types)
|
|
|
|
if type_type == "native" or depth <= 0: # native type
|
|
return get_random_native_type()
|
|
elif type_type == "collection":
|
|
return get_random_collection_type(cql, keyspace, depth, udts, frozen)
|
|
elif type_type == "tuple":
|
|
return get_random_tuple_type(cql, keyspace, depth, udts, frozen)
|
|
elif type_type == "UDT":
|
|
udt = random.choice(udts)
|
|
return f"frozen<{udt}>" if frozen else udt
|
|
|
|
def new_random_keyspace(cql):
|
|
strategies = ["SimpleStrategy", "NetworkTopologyStrategy"]
|
|
writes = ["true", "false"]
|
|
|
|
options = {}
|
|
options["class"] = random.choice(strategies)
|
|
options["replication_factor"] = random.randrange(1, 6)
|
|
options_str = ", ".join([f"'{k}': '{v}'" for (k, v) in options.items()])
|
|
extra = ""
|
|
if options["class"] == "NetworkTopologyStrategy" and options["replication_factor"] != 1:
|
|
extra = " and tablets = { 'enabled': false }"
|
|
|
|
write = random.choice(writes)
|
|
return new_test_keyspace(cql, f"with replication = {{{options_str}}} and durable_writes = {write}{extra}")
|
|
|
|
# A utility function for creating random table. `udts` argument represents
|
|
# UDTs that can be used to create the table. The function uses `new_test_table`
|
|
# from util.py, so it can be used in a "with", as:
|
|
# with new_random_table(cql, test_keyspace) as table:
|
|
def new_random_table(cql, keyspace, udts=[]):
|
|
pk_n = random.randrange(1, max_pk)
|
|
ck_n = random.randrange(max_ck)
|
|
regular_n = random.randrange(1, max_regular)
|
|
static_n = random.randrange(max_static if ck_n > 0 else 1)
|
|
|
|
pk = [(unique_name(), get_random_type(cql, keyspace, max_depth, udts, True)) for _ in range(pk_n)]
|
|
ck = [(unique_name(), get_random_type(cql, keyspace, max_depth, udts, True)) for _ in range(ck_n)]
|
|
pk_fields = ", ".join([f"{n} {t}" for (n, t) in pk])
|
|
ck_fields = ", ".join([f"{n} {t}" for (n, t) in ck])
|
|
|
|
partition_key = ", ".join([n for (n, _) in pk])
|
|
clustering_fields = ", ".join([n for (n, _) in ck])
|
|
clustering_key = f", {clustering_fields}" if ck_n > 0 else ""
|
|
primary_def = f"PRIMARY KEY(({partition_key}){clustering_key})"
|
|
|
|
|
|
schema = ""
|
|
extras = {}
|
|
if random.random() <= counter_table_chance:
|
|
counter_name = unique_name()
|
|
schema = f"{pk_fields}, {ck_fields}, {counter_name} counter, {primary_def}"
|
|
else:
|
|
regular = [(unique_name(), get_random_type(cql, keyspace, max_depth, udts)) for _ in range(regular_n)]
|
|
static = [(unique_name(), get_random_type(cql, keyspace, max_depth, udts)) for _ in range(static_n)]
|
|
regular_fields = ", ".join([f"{n} {t}" for (n, t) in regular])
|
|
static_fields = ", ".join([f"{n} {t} static" for (n, t) in static])
|
|
schema = f"{pk_fields}, {ck_fields}, {regular_fields}, {static_fields}, {primary_def}"
|
|
|
|
extras["default_time_to_live"] = random.randrange(1000000)
|
|
|
|
|
|
caching_keys = ["ALL", "NONE"]
|
|
caching_rows = ["ALL", "NONE", random.randrange(1000000)]
|
|
caching_k = random.choice(caching_keys)
|
|
caching_r = random.choice(caching_rows)
|
|
extras["caching"] = f"{{'keys':'{caching_k}', 'rows_per_partition':'{caching_r}'}}"
|
|
|
|
compactions = ["SizeTieredCompactionStrategy", "TimeWindowCompactionStrategy", "LeveledCompactionStrategy"]
|
|
extras["compaction"] = f"{{'class': '{random.choice(compactions)}'}}"
|
|
|
|
speculative_retries = ["ALWAYS", "NONE", "99.0PERCENTILE", "50.00ms"]
|
|
extras["speculative_retry"] = f"'{random.choice(speculative_retries)}'"
|
|
|
|
compressions = ["LZ4Compressor", "SnappyCompressor", "DeflateCompressor"]
|
|
extras["compression"] = f"{{'sstable_compression': '{random.choice(compressions)}'}}"
|
|
|
|
# see the last element of `probs` defined by scylladb/utils/bloom_calculation.cc,
|
|
# the minimum false positive rate supported by the bloom filter is determined by
|
|
# prob's element with the greatest number of buckets. also, because random.random()
|
|
# could return 0.0, not to mention round() could round a small enough float to 0,
|
|
# let's add an epislon to ensure that the randomly picked chance is not equal to the
|
|
# minimum.
|
|
min_supported_bloom_filter_fp_chance = 6.71e-5 + 1e-5
|
|
extras["bloom_filter_fp_chance"] = max(min_supported_bloom_filter_fp_chance, round(random.random(), 5))
|
|
extras["crc_check_chance"] = round(random.random(), 5)
|
|
extras["gc_grace_seconds"] = random.randrange(100000, 1000000)
|
|
min_idx_interval = random.randrange(100, 1000)
|
|
extras["min_index_interval"] = min_idx_interval
|
|
extras["max_index_interval"] = random.randrange(min_idx_interval, 10000)
|
|
extras["memtable_flush_period_in_ms"] = random.randrange(0, 10000)
|
|
|
|
if is_scylla(cql):
|
|
# Extra properties which ScyllaDB supports but Cassandra doesn't
|
|
extras["paxos_grace_seconds"] = random.randrange(1000, 100000)
|
|
extras["tombstone_gc"] = f"{{'mode': 'timeout', 'propagation_delay_in_seconds': '{random.randrange(100, 100000)}'}}"
|
|
|
|
extra_options = [f"{k} = {v}" for (k, v) in extras.items()]
|
|
extra_str = " AND ".join(extra_options)
|
|
extra = f" WITH {extra_str}"
|
|
return new_test_table(cql, keyspace, schema, extra)
|
|
|
|
# A utility function for creating random UDT. `udts` argument represents
|
|
# other UDTs that can be used to create the new one. The function uses `new_type`
|
|
# from util.py, so it can be used in a "with", as:
|
|
# with new_random_type(cql, test_keyspace) as udt:
|
|
def new_random_type(cql, keyspace, udts=[]):
|
|
n = random.randrange(1, max_types+1)
|
|
types = [get_random_type(cql, keyspace, max_depth, udts, True) for _ in range(n)]
|
|
fields = ", ".join([f"{unique_name()} {t}" for t in types])
|
|
|
|
return new_type(cql, keyspace, f"({fields})")
|
|
|
|
### ===========================================================================
|