mirror of
https://github.com/scylladb/scylladb.git
synced 2026-04-27 03:45:11 +00:00
Recently, some users have seen "Key size too large" errors in various places. Cassandra and Scylla impose a 64KB length limit on keys, and we have known about bugs in this area for a long time - and even had some translated Cassandra unit tests that cover some of them. But these tests did not cover all the corner cases and left us with partial and fragmented knowledge of this problem, spread over many test files and many issues. In this patch, we add a single test file, test/cql-pytest/test_key_length.py which attempts to rigourously explore the various bugs we have with CQL key length limits. These test aim to reproduce all known bugs in this area: * Refs #3017 - CQL layer accepts set values too large to be written to an sstable * Refs #10366 - Enforce Key-length limits during SELECT * Refs #12247 - Better error reporting for oversized keys during INSERT * Refs #16772 - Key length should be limited to exactly 65535, not less The following less interesting bug is already covered by many tests so I decided not to test it again: * Refs #7745 - Length of map keys and set items are incorrectly limited to 64K in unprepared CQL There's also a situation in materialized views and secondary indexes, where a column that was _not_ a key, now becomes a key, and a length limit needs to be enforced on it. We already have good test coverage for this (in test/cql-pytest/test_secondary_index.py and in test/cql-pytest/test_materialized_view.py), and we have an issue: * Refs #8627 - Cleanly reject updates with indexed values where value > 64k All 16 tests added here pass on Cassandra 5 except one that fails on https://issues.apache.org/jira/browse/CASSANDRA-19270, but 11 of the tests currently fail on Scylla (6 on #12247, 2 on #10366, 3 on #16772). It is possible that our decision in #16772 will not be to fix Scylla to match Cassandra but rather to declare that strict compatibility isn't needed in this case or even that Cassandra is wrong. But even then, having these tests which demonstrate the behavior of both Cassandra and Scylla will be important. Signed-off-by: Nadav Har'El <nyh@scylladb.com> Closes scylladb/scylladb#16779
301 lines
15 KiB
Python
301 lines
15 KiB
Python
# Copyright 2024-present ScyllaDB
|
|
#
|
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
#############################################################################
|
|
# Although regular column values are only limited by 31 bits (2GB),
|
|
# in Cassandra and Scylla keys are limited to 16 bits - 65535 bytes.
|
|
#
|
|
# This file contains several tests that check various cases of this
|
|
# key length limit - what exactly does this limit include (compound keys?
|
|
# clustering column name?) and how attempts to insert or select with
|
|
# oversized keys are handled (naturally, we want a clear error, not some
|
|
# sort of hang or crash).
|
|
#
|
|
# There are also cases - namely materialized views and secondary index -
|
|
# where a regular column of potentially 2GB size is used as a key, and
|
|
# needs to be limited to 64KB. We have tests for this (see issue #8627)
|
|
# in test_secondary_index.py
|
|
#############################################################################
|
|
|
|
import pytest
|
|
from util import new_test_table, random_string, unique_key_string, unique_key_int
|
|
import nodetool
|
|
from cassandra.protocol import InvalidRequest
|
|
from cassandra.util import SortedSet
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def table1(cql, test_keyspace):
|
|
with new_test_table(cql, test_keyspace, "p text, c text, v int, PRIMARY KEY (p, c)") as table:
|
|
yield table
|
|
|
|
# Trying to insert a 65KB (clearly over 64KB) value to a clustering key or
|
|
# partition key should fail, with a clean InvalidRequest - not some internal
|
|
# server error or worse.
|
|
# Reproduces #12247
|
|
@pytest.mark.xfail(reason="Issue #12247")
|
|
def test_insert_65k_pk(cql, table1):
|
|
stmt = cql.prepare(f'INSERT INTO {table1} (p, c) VALUES (?,?)')
|
|
# Cassandra writes: "Key length of 66560 is longer than maximum of 65535"
|
|
with pytest.raises(InvalidRequest, match='Key length'):
|
|
cql.execute(stmt, ['x'*(65*1024), 'hello'])
|
|
|
|
# Reproduces #12247
|
|
@pytest.mark.xfail(reason="Issue #12247")
|
|
def test_insert_65k_ck(cql, table1):
|
|
stmt = cql.prepare(f'INSERT INTO {table1} (p, c) VALUES (?,?)')
|
|
# Cassandra writes: "Key length of 66560 is longer than maximum of 65535"
|
|
with pytest.raises(InvalidRequest, match='Key length'):
|
|
cql.execute(stmt, ['hello', 'x'*(65*1024)])
|
|
|
|
# Trying to read with a 65KB (clearly over 64KB) clustering key or partition
|
|
# key should fail, with a clean InvalidRequest - not some internal server
|
|
# error or worse.
|
|
# Reproduces #10366.
|
|
@pytest.mark.xfail(reason="Issue #10366")
|
|
def test_where_65k_pk(cql, table1):
|
|
stmt = cql.prepare(f'SELECT * FROM {table1} WHERE p = ?')
|
|
# Cassandra writes: "Key length of 66560 is longer than maximum of 65535"
|
|
with pytest.raises(InvalidRequest, match='Key length'):
|
|
cql.execute(stmt, ['x'*(65*1024)])
|
|
|
|
# While "WHERE p = ?" gives an InvalidRequest error on an oversized key (as
|
|
# the previous test checks), in Cassandra the clustering-key version
|
|
# "WHERE c = ?" doesn't throw, and just silently returns no matches.
|
|
# I don't want to enshrine this inconsistent behavior, and the following
|
|
# test allows this case to either throw a clean InvalidRequest or silently
|
|
# return no match as Cassandra does. In any case, returning some ugly internal
|
|
# error (as happened in Scylla) is a bug.
|
|
# Reproduces #10366.
|
|
@pytest.mark.xfail(reason="Issue #10366")
|
|
def test_where_65k_ck(cql, table1):
|
|
stmt = cql.prepare(f'SELECT * FROM {table1} WHERE p = ? AND c = ?')
|
|
try:
|
|
assert [] == list(cql.execute(stmt, ['dog', 'x'*(65*1024)]))
|
|
except InvalidRequest:
|
|
# Acceptable as an alternative to Cassandra's silently returning
|
|
# nothing (see explanation above)
|
|
pass
|
|
|
|
# What is the exact size limit of the key? It turns out that Cassandra allows
|
|
# the partition key length to reach all the way to 65535 (2^16-1), and the
|
|
# limit also applies for clustering key length (here, we are talking about
|
|
# the case of a single-component key). The following two tests check that
|
|
# such a 65535-byte partition and clustering key (respectively) can be
|
|
# written and then correctly read back.
|
|
# Scylla shouldn't impose its own smaller size limit - but in issue #16772,
|
|
# it did (the limit was 65533).
|
|
|
|
@pytest.mark.xfail(reason="Issue #16772")
|
|
def test_insert_65535_pk(cql, table1):
|
|
stmt = cql.prepare(f'INSERT INTO {table1} (p, c) VALUES (?,?)')
|
|
p = random_string(length=65535)
|
|
c = unique_key_string() # not particularly long
|
|
cql.execute(stmt, [p, c])
|
|
stmt = cql.prepare(f'SELECT p,c FROM {table1} WHERE p=?')
|
|
assert list(cql.execute(stmt, [p])) == [(p, c)]
|
|
|
|
@pytest.mark.xfail(reason="Issue #16772")
|
|
def test_insert_65535_ck(cql, table1):
|
|
stmt = cql.prepare(f'INSERT INTO {table1} (p, c) VALUES (?,?)')
|
|
p = unique_key_string() # not particularly long
|
|
c = random_string(length=65535)
|
|
cql.execute(stmt, [p, c])
|
|
stmt = cql.prepare(f'SELECT p,c FROM {table1} WHERE p=?')
|
|
assert list(cql.execute(stmt, [p])) == [(p, c)]
|
|
|
|
# Same as test_insert_65535_ck() but also set a regular column. Scylla's
|
|
# implementation docs/architecture/sstable/sstable2/sstable-interpretation.rst
|
|
# encodes an cell's key to contain the clustering key *value* as the
|
|
# first component, and the regular column *name* as the second component.
|
|
# This should work - Scylla shouldn't try to limit the sum of these lengths
|
|
# or something.
|
|
@pytest.mark.xfail(reason="Issue #16772")
|
|
def test_insert_65535_ck_with_regular_column(cql, table1):
|
|
stmt = cql.prepare(f'INSERT INTO {table1} (p, c, v) VALUES (?,?,?)')
|
|
p = unique_key_string() # not particularly long
|
|
c = random_string(length=65535)
|
|
v = unique_key_int()
|
|
cql.execute(stmt, [p, c, v])
|
|
stmt = cql.prepare(f'SELECT p,c,v FROM {table1} WHERE p=?')
|
|
assert list(cql.execute(stmt, [p])) == [(p, c, v)]
|
|
|
|
# The following are tests for compound partition key and composite clustering
|
|
# key - i.e., keys containing multiple columns.
|
|
# Individual components cannot be too long, but there are also limits on
|
|
# the total size, and the compound format has some overheads which limits
|
|
# the maximum size, and we want to check all of this below.
|
|
|
|
@pytest.fixture(scope="module")
|
|
def table2(cql, test_keyspace):
|
|
with new_test_table(cql, test_keyspace, "p1 text, p2 text, c1 text, c2 text, PRIMARY KEY ((p1, p2), c1, c2)") as table:
|
|
yield table
|
|
|
|
# As we checked above for single-component keys, a 65 KB key is definitely
|
|
# oversized and no key component can be this big.
|
|
# Cassandra also has a bug (see CASSANDRA-19270) in the compound pk case -
|
|
# instead of the expected InvalidRequest error, it generates an internal
|
|
# server error (i.e., NoHostAvailable) with the message "'H' format requires
|
|
# 0 <= number <= 65535".
|
|
@pytest.mark.xfail(reason="Issue #12247")
|
|
def test_insert_65k_pk_compound(cql, table2, cassandra_bug):
|
|
stmt = cql.prepare(f'INSERT INTO {table2} (p1, p2, c1, c2) VALUES (?,?,?,?)')
|
|
big = 'x'*(65*1024)
|
|
with pytest.raises(InvalidRequest, match='Key length'):
|
|
cql.execute(stmt, [big, 'dog', 'cat', 'mouse'])
|
|
with pytest.raises(InvalidRequest, match='Key length'):
|
|
cql.execute(stmt, ['dog', big, 'cat', 'mouse'])
|
|
|
|
@pytest.mark.xfail(reason="Issue #12247")
|
|
def test_insert_65k_ck_composite(cql, table2):
|
|
stmt = cql.prepare(f'INSERT INTO {table2} (p1, p2, c1, c2) VALUES (?,?,?,?)')
|
|
big = 'x'*(65*1024)
|
|
with pytest.raises(InvalidRequest, match='Key length'):
|
|
cql.execute(stmt, ['dog', 'cat', big, 'mouse'])
|
|
with pytest.raises(InvalidRequest, match='Key length'):
|
|
cql.execute(stmt, ['dog', 'cat', 'mouse', big])
|
|
|
|
# The above test_insert_65535_pk tried to reach the limit 65535 bytes in the
|
|
# case of a single-component partition key, and checked that see we can
|
|
# actually use a string of that size as a key. This test tries to reach the
|
|
# limit with a *compound* partition key of two components. Unsurprisingly
|
|
# (because of how partition keys are serialized), we see that what is limited
|
|
# is the total size the compound partition key, not individual components.
|
|
# In Scylla, we can reach 65535-4 bytes of keys, apparently two extra bytes
|
|
# (lengths?) are used per component, but in Cassandra the limit we can reach
|
|
# with two key components is 65535-6, perhaps there's an additional count
|
|
# of components?
|
|
#
|
|
# of components?). It's not at all clear that this is
|
|
# correct (see issue #16772) but we'll enshirine it for the moment because
|
|
# Cassandra is even more broken in this case :-(
|
|
def test_insert_total_compound_pk_ok(cql, table2):
|
|
stmt = cql.prepare(f'INSERT INTO {table2} (p1, p2, c1, c2) VALUES (?,?,?,?)')
|
|
# A total compound pk of size 65535-6 will work (in Scylla, even -4 works)
|
|
length = 65535 - 6
|
|
p1 = random_string(length=100)
|
|
p2 = random_string(length=(length-len(p1)))
|
|
c1 = unique_key_string() # not particularly long
|
|
c2 = unique_key_string() # not particularly long
|
|
cql.execute(stmt, [p1, p2, c1, c2])
|
|
stmt = cql.prepare(f'SELECT * FROM {table2} WHERE p1=? AND p2=?')
|
|
assert list(cql.execute(stmt, [p1, p2])) == [(p1, p2, c1, c2)]
|
|
|
|
@pytest.mark.xfail(reason="Issue #12247")
|
|
def test_insert_total_compound_pk_err(cql, table2):
|
|
stmt = cql.prepare(f'INSERT INTO {table2} (p1, p2, c1, c2) VALUES (?,?,?,?)')
|
|
# A total compound pk of size 65536 is definitely too long
|
|
length = 65536
|
|
p1 = random_string(length=length//2)
|
|
p2 = p1
|
|
c1 = unique_key_string() # not particularly long
|
|
c2 = unique_key_string() # not particularly long
|
|
# In issue #12247, Scylla returned an internal server error instead of
|
|
# a clean InvalidRequest here.
|
|
with pytest.raises(InvalidRequest, match='Key length'):
|
|
cql.execute(stmt, [p1, p2, c1, c2])
|
|
|
|
# The case of a composite clustering key (clustering key composed of multiple
|
|
# columns) is similar to the case we saw above of compound partition key -
|
|
# again the *total* size of the key is limited, not the size of individual
|
|
# components. Strangely, this limit wasn't enforced like that in Cassandra 4 -
|
|
# it only limited individual components, not the sum - but it was changed in
|
|
# Cassandra 5 as an incidental part of adding a Vector type,
|
|
# https://github.com/apache/cassandra/commit/ae537abc6494564d7254a2126465522d86b44c1e
|
|
# Scylla always limited the total size (see discussion in issue #16772).
|
|
#
|
|
# As in the the compound partition key case above, also here there is a
|
|
# slight difference on what is counted towards this limit: In Scylla two
|
|
# components can only reach 65535-4 (i.e., a 2-byte length for each component)
|
|
# but in Cassandra the sum of the two components can reach all the way to
|
|
# 65535. I suspect this might even be a Cassandra bug (if there's a reason
|
|
# why the sum is limited, it should include everything, including length
|
|
# bytes).
|
|
def test_insert_total_composite_ck_ok(cql, table2):
|
|
stmt = cql.prepare(f'INSERT INTO {table2} (p1, p2, c1, c2) VALUES (?,?,?,?)')
|
|
# A total composite ck of size 65535-4 will work (in Cassandra 5, even
|
|
# 65535 works, I suspect this might be a bug)
|
|
length = 65535 - 4
|
|
c1 = random_string(length=100)
|
|
c2 = random_string(length=(length-len(c1)))
|
|
p1 = unique_key_string() # not particularly long
|
|
p2 = unique_key_string() # not particularly long
|
|
cql.execute(stmt, [p1, p2, c1, c2])
|
|
stmt = cql.prepare(f'SELECT * FROM {table2} WHERE p1=? AND p2=?')
|
|
assert list(cql.execute(stmt, [p1, p2])) == [(p1, p2, c1, c2)]
|
|
|
|
@pytest.mark.xfail(reason="Issue #12247")
|
|
def test_insert_total_composite_ck_err(cql, table2):
|
|
stmt = cql.prepare(f'INSERT INTO {table2} (p1, p2, c1, c2) VALUES (?,?,?,?)')
|
|
# A total composite ck of size 65536 is definitely too long
|
|
length = 65536
|
|
c1 = random_string(length=length//2)
|
|
c2 = c1
|
|
p1 = unique_key_string() # not particularly long
|
|
p2 = unique_key_string() # not particularly long
|
|
# In issue #12247, Scylla returned an internal server error instead of
|
|
# a clean InvalidRequest here.
|
|
with pytest.raises(InvalidRequest, match='Key length'):
|
|
cql.execute(stmt, [p1, p2, c1, c2])
|
|
|
|
# The following tests check length limits on parts of collections.
|
|
# These tests belong in this file, i.e., are related to the limit on key
|
|
# length, because in some cases collection keys are represented internally
|
|
# in the same way as clustering keys - see
|
|
# docs/architecture/sstable/sstable2/sstable-interpretation.rst.
|
|
|
|
@pytest.fixture(scope="module")
|
|
def table3(cql, test_keyspace):
|
|
with new_test_table(cql, test_keyspace, "p int PRIMARY KEY, set_column set<text>, list_column list<text>, map_column map<text,text>") as table:
|
|
yield table
|
|
|
|
# Ever since CASSANDRA-10374 was fixed in 2015, set values are no longer
|
|
# limited to 64KB - and even though internally they behave like column names,
|
|
# which in the past were limited to 64KB, now it should work beyond 64KB.
|
|
# Reproduces issue #3017 where Scylla did the worst of both worlds: We allowed
|
|
# oversized set values, but then crashed on compaction.
|
|
# Note that because of #7745, this test needs to use prepared statements
|
|
# for the writes.
|
|
def test_set_65k(cql, table3):
|
|
stmt = cql.prepare(f'UPDATE {table3} SET set_column = set_column + ? WHERE p = ?')
|
|
p = unique_key_int()
|
|
val = random_string(length=65*1024)
|
|
cql.execute(stmt, [{val}, p])
|
|
results = list(cql.execute(f'SELECT p, set_column FROM {table3} WHERE p={p}'))
|
|
assert results == [(p, SortedSet([val]))]
|
|
# Reproduces issue #3017: After the above code worked well, and correctly
|
|
# supported the more-than-64KB set value, if we now write it to an sstable
|
|
# the compact() will crash Scylla.
|
|
nodetool.flush(cql, table3)
|
|
nodetool.compact(cql, table3)
|
|
|
|
# For lists, it's even more obvious that the size of the value is not
|
|
# limited to 64KB, because it's *not* represented as a column name.
|
|
def test_list_65k(cql, table3):
|
|
stmt = cql.prepare(f'UPDATE {table3} SET list_column = list_column + ? WHERE p = ?')
|
|
p = unique_key_int()
|
|
val = random_string(length=65*1024)
|
|
cql.execute(stmt, [[val], p])
|
|
results = list(cql.execute(f'SELECT p, list_column FROM {table3} WHERE p={p}'))
|
|
assert results == [(p, [val])]
|
|
|
|
# Map keys behave like set values, similar to clustering keys, so it's
|
|
# important to check that they really allow more than 64KB, even with
|
|
# flush and compact.
|
|
# Reproduces #3017 where Scylla did the worst of both worlds: It allowed
|
|
# oversized map keys, but then crashed on compaction.
|
|
def test_map_65k(cql, table3):
|
|
stmt = cql.prepare(f'UPDATE {table3} SET map_column[?] = ? WHERE p = ?')
|
|
p = unique_key_int()
|
|
key = random_string(length=65*1024)
|
|
val = random_string(length=65*1024)
|
|
cql.execute(stmt, [key, val, p])
|
|
results = list(cql.execute(f'SELECT p, map_column FROM {table3} WHERE p={p}'))
|
|
assert results == [(p, {key: val})]
|
|
# Reproduces issue #3017: After the above code worked well, and correctly
|
|
# supported the more-than-64KB map key, if we now write it to an sstable
|
|
# the compact() will crash Scylla.
|
|
nodetool.flush(cql, table3)
|
|
nodetool.compact(cql, table3)
|