Files
scylladb/test/cqlpy/test_key_length.py
Piotr Smaron 71542206bc cql: return InvalidRequest for oversized partition/clustering keys
When a partition key or clustering key value exceeds the 64 KiB limit
(65535 bytes serialized), Scylla used to raise a generic
std::runtime_error "Key size too large: N > M" from the low-level
compound-key serializer. That error surfaced to clients as a CQL
server error (code 0x0000, "NoHostAvailable"-looking), which is both
ugly and incompatible with Cassandra - Cassandra returns a clean
InvalidRequest with the message "Key length of N is longer than
maximum of M".

Fix this at the single chokepoint: compound_type::serialize_value in
keys/compound.hh. The serializer is on every path that materializes a
key - INSERT/UPDATE/DELETE/BATCH build mutations through it, and
SELECT builds partition and clustering ranges through it - so a single
throw replacement produces a clean InvalidRequest consistently across
all paths and all key shapes (single, compound PK, composite CK).

The previous approach on this PR branch patched three call sites in
cql3/restrictions/statement_restrictions.cc, which only covered
SELECT, duplicated the check, and placed it mid-restrictions code
(flagged in review). Dropping those changes in favour of the
root-cause fix here.

Un-xfail the tests this fixes:
- test/cqlpy/test_key_length.py: test_insert_65k_pk, test_insert_65k_ck,
  test_where_65k_pk, test_where_65k_ck, test_insert_65k_ck_composite,
  test_insert_total_compound_pk_err, test_insert_total_composite_ck_err.
- test/cqlpy/cassandra_tests/.../insert_test.py: testPKInsertWithValueOver64K,
  testCKInsertWithValueOver64K.
- test/cqlpy/cassandra_tests/.../select_test.py: testPKQueryWithValueOver64K.

test_insert_65k_pk_compound stays xfail: its oversized value gets
rejected by the Python driver's CQL wire-protocol encoder (see
CASSANDRA-19270) before reaching the server, so the fix can't apply.
Updated its reason. testCKQueryWithValueOver64K stays xfail with an
updated reason: Cassandra silently returns empty for an oversized
clustering key in WHERE, while Scylla now throws InvalidRequest - a
deliberate choice mirroring the partition-key case, documented in
the discussion on #10366.

Add three tight-boundary tests (addressing review feedback on the
previous revision) that pin MAX+1 behaviour for SELECT and INSERT of
both partition and clustering keys.

Update test/cluster/dtest/limits_test.py to match the new message
("Key length of \\d+ is longer than maximum of 65535").

fixes #10366
fixes #12247

Co-authored-by: Alexander Turetskiy <someone.tur@gmail.com>

Closes scylladb/scylladb#23433
2026-05-11 16:56:35 +03:00

326 lines
16 KiB
Python

# Copyright 2024-present ScyllaDB
#
# SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.1
#############################################################################
# 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
from . 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
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
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.
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.
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).
# Tight-boundary SELECT tests for #10366: values at MAX-allowed (MAX-2 for
# Scylla because of the 2-byte framing overhead per component) must not
# error out during SELECT; values at MAX-allowed+1 must raise a clean
# InvalidRequest. The MAX-allowed case for the 65533/65535 discrepancy is
# tracked by #16772 and remains xfail; here we just pin the failure side
# of the boundary so a regression would be caught immediately.
def test_select_at_max_pk_plus_one_fails(cql, table1):
stmt = cql.prepare(f'SELECT * FROM {table1} WHERE p = ?')
over = 'x' * (65535 + 1)
with pytest.raises(InvalidRequest, match='Key length'):
cql.execute(stmt, [over])
def test_insert_at_max_pk_plus_one_fails(cql, table1):
stmt = cql.prepare(f'INSERT INTO {table1} (p, c) VALUES (?,?)')
over = 'x' * (65535 + 1)
with pytest.raises(InvalidRequest, match='Key length'):
cql.execute(stmt, [over, 'c'])
def test_insert_at_max_ck_plus_one_fails(cql, table1):
stmt = cql.prepare(f'INSERT INTO {table1} (p, c) VALUES (?,?)')
over = 'x' * (65535 + 1)
with pytest.raises(InvalidRequest, match='Key length'):
cql.execute(stmt, ['p', over])
@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.
# In the *compound* partition-key case the Python driver rejects the
# oversized component locally, before the request reaches the server: when
# computing the token-aware routing key it serializes each compound key
# component with a 2-byte length prefix:
# cassandra/query.py:304:
# yield struct.pack(">H%dsB" % l, l, p, 0)
# For l > 65535 this raises struct.error("'H' format requires 0 <= number
# <= 65535"), which the driver re-raises as NoHostAvailable - not as a
# server-side InvalidRequest. The server-side fix in this PR for #12247
# therefore cannot make this test pass; it stays xfail until the driver
# either lifts the limit or surfaces the error as InvalidRequest. The
# matching upstream issue is CASSANDRA-19270.
@pytest.mark.xfail(reason="Driver rejects oversized compound-pk component (struct.pack '>H'); see CASSANDRA-19270, #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'])
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)]
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)]
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)