Skip to content

Commit 5fe1902

Browse files
committed
DRIVER-153: tests for SCYLLA_USE_METADATA_ID extension
test_protocol_features.py: - test_use_metadata_id_parsing: SCYLLA_USE_METADATA_ID parsed from SUPPORTED - test_use_metadata_id_missing: use_metadata_id False when key absent - test_use_metadata_id_startup_options: key present in STARTUP when negotiated - test_use_metadata_id_not_in_startup_when_not_negotiated: absent otherwise test_protocol.py: - test_execute_message_skip_meta_flag: _SKIP_METADATA_FLAG (0x02) is written - test_execute_message_scylla_metadata_id_v4: result_metadata_id written on v4 when use_metadata_id=True (Scylla extension) - test_execute_message_scylla_metadata_id_none_writes_sentinel: extension active but result_metadata_id=None writes empty sentinel b'' (LWT / mixed cluster); frame layout preserved - test_execute_message_v5_metadata_id_none_writes_sentinel: v5 with result_metadata_id=None writes empty sentinel instead of TypeError crash - test_recv_results_prepared_scylla_extension_reads_metadata_id - test_recv_results_prepared_no_extension_skips_metadata_id - test_recv_results_metadata_changed_flag - test_recv_results_metadata_no_metadata_flag_skips_metadata_id test_response_future.py: _set_result METADATA_CHANGED path: - test_set_result_updates_metadata_when_metadata_changed - test_set_result_does_not_update_metadata_when_metadata_id_absent - test_set_result_warns_when_metadata_id_but_no_column_metadata _query per-connection feature gating (5 scenarios): - test_query_sets_skip_meta_with_scylla_extension - test_query_no_skip_meta_without_extension - test_query_no_skip_meta_when_prepared_statement_has_no_metadata_id - test_query_sets_skip_meta_for_protocol_v5 - test_query_no_skip_meta_when_result_metadata_is_none (LWT guard)
1 parent 82051a3 commit 5fe1902

3 files changed

Lines changed: 488 additions & 1 deletion

File tree

tests/unit/test_protocol.py

Lines changed: 162 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import io
16+
import struct
1517
import unittest
1618

1719
from unittest.mock import Mock
@@ -21,8 +23,11 @@
2123
PrepareMessage, QueryMessage, ExecuteMessage, UnsupportedOperation,
2224
_PAGING_OPTIONS_FLAG, _WITH_SERIAL_CONSISTENCY_FLAG,
2325
_PAGE_SIZE_FLAG, _WITH_PAGING_STATE_FLAG,
24-
BatchMessage
26+
_SKIP_METADATA_FLAG,
27+
BatchMessage, ResultMessage,
28+
RESULT_KIND_ROWS
2529
)
30+
from cassandra.protocol_features import ProtocolFeatures
2631
from cassandra.query import BatchType
2732
from cassandra.marshal import uint32_unpack
2833
from cassandra.cluster import ContinuousPagingOptions
@@ -68,6 +73,162 @@ def test_execute_message(self):
6873
(b'\x00\x04',),
6974
(b'\x00\x00\x00\x01',), (b'\x00\x00',)])
7075

76+
def test_execute_message_skip_meta_flag(self):
77+
"""skip_meta=True must set _SKIP_METADATA_FLAG (0x02) in the flags byte."""
78+
message = ExecuteMessage('1', [], 4, skip_meta=True)
79+
mock_io = Mock()
80+
81+
message.send_body(mock_io, 4)
82+
# flags byte should be VALUES_FLAG | SKIP_METADATA_FLAG = 0x01 | 0x02 = 0x03
83+
self._check_calls(mock_io, [(b'\x00\x01',), (b'1',), (b'\x00\x04',), (b'\x03',), (b'\x00\x00',)])
84+
85+
def test_execute_message_scylla_metadata_id_v4(self):
86+
"""result_metadata_id should be written on protocol v4 when use_metadata_id=True (Scylla extension)."""
87+
message = ExecuteMessage('1', [], 4)
88+
message.result_metadata_id = b'foo'
89+
message.use_metadata_id = True
90+
mock_io = Mock()
91+
92+
message.send_body(mock_io, 4)
93+
# metadata_id written before query params (same position as v5)
94+
self._check_calls(mock_io, [(b'\x00\x01',), (b'1',),
95+
(b'\x00\x03',), (b'foo',),
96+
(b'\x00\x04',), (b'\x01',), (b'\x00\x00',)])
97+
98+
def test_execute_message_scylla_metadata_id_none_writes_sentinel(self):
99+
"""
100+
When use_metadata_id=True but result_metadata_id is None (e.g. LWT statement or
101+
mixed cluster), send_body must still write the field as an empty string sentinel
102+
(\\x00\\x00) so the frame layout matches what the server expects.
103+
"""
104+
message = ExecuteMessage('1', [], 4)
105+
message.use_metadata_id = True
106+
# result_metadata_id intentionally left as None
107+
mock_io = Mock()
108+
109+
message.send_body(mock_io, 4)
110+
# empty sentinel: \x00\x00 (zero-length short) + b'' (zero bytes), then normal query params
111+
self._check_calls(mock_io, [(b'\x00\x01',), (b'1',),
112+
(b'\x00\x00',), (b'',),
113+
(b'\x00\x04',), (b'\x01',), (b'\x00\x00',)])
114+
115+
def test_execute_message_v5_metadata_id_none_writes_sentinel(self):
116+
"""
117+
On protocol v5, result_metadata_id is always written (uses_prepared_metadata).
118+
When result_metadata_id is None (e.g. LWT statement or mixed cluster where the
119+
statement was prepared before the extension was active), send_body must write an
120+
empty sentinel instead of crashing with TypeError.
121+
"""
122+
message = ExecuteMessage('1', [], 4)
123+
# result_metadata_id intentionally left as None; use_metadata_id stays False (v5 native path)
124+
mock_io = Mock()
125+
126+
message.send_body(mock_io, 5)
127+
# v5 always writes metadata_id: None → empty sentinel \x00\x00 + b'', then query params
128+
# v5 uses 4-byte flags: VALUES_FLAG = \x00\x00\x00\x01
129+
self._check_calls(mock_io, [(b'\x00\x01',), (b'1',),
130+
(b'\x00\x00',), (b'',),
131+
(b'\x00\x04',),
132+
(b'\x00\x00\x00\x01',), (b'\x00\x00',)])
133+
134+
def test_recv_results_prepared_scylla_extension_reads_metadata_id(self):
135+
"""
136+
When use_metadata_id is True (Scylla extension), result_metadata_id must be
137+
read from the PREPARE response even for protocol v4.
138+
"""
139+
# Build a minimal valid PREPARE response binary (no bind/result columns):
140+
# query_id: short(2) + b'ab'
141+
# result_metadata_id: short(3) + b'xyz' <-- only present when extension active
142+
# prepared flags: int(1) = global_tables_spec
143+
# colcount: int(0)
144+
# num_pk_indexes: int(0)
145+
# ksname: short(2) + b'ks'
146+
# cfname: short(2) + b'tb'
147+
# result flags: int(4) = no_metadata
148+
# result colcount: int(0)
149+
buf = io.BytesIO(
150+
struct.pack('>H', 2) + b'ab' # query_id
151+
+ struct.pack('>H', 3) + b'xyz' # result_metadata_id
152+
+ struct.pack('>i', 1) # prepared flags: global_tables_spec
153+
+ struct.pack('>i', 0) # colcount = 0
154+
+ struct.pack('>i', 0) # num_pk_indexes = 0
155+
+ struct.pack('>H', 2) + b'ks' # ksname
156+
+ struct.pack('>H', 2) + b'tb' # cfname
157+
+ struct.pack('>i', 4) # result flags: no_metadata
158+
+ struct.pack('>i', 0) # result colcount = 0
159+
)
160+
161+
features_with_extension = ProtocolFeatures(use_metadata_id=True)
162+
msg = ResultMessage(kind=4) # RESULT_KIND_PREPARED = 4
163+
msg.recv_results_prepared(buf, protocol_version=4,
164+
protocol_features=features_with_extension,
165+
user_type_map={})
166+
assert msg.query_id == b'ab'
167+
assert msg.result_metadata_id == b'xyz'
168+
169+
def test_recv_results_prepared_no_extension_skips_metadata_id(self):
170+
"""
171+
Without use_metadata_id, result_metadata_id must NOT be read on protocol v4.
172+
The buffer must NOT contain a metadata_id field.
173+
"""
174+
buf = io.BytesIO(
175+
struct.pack('>H', 2) + b'ab' # query_id
176+
# no result_metadata_id
177+
+ struct.pack('>i', 1) # prepared flags: global_tables_spec
178+
+ struct.pack('>i', 0) # colcount = 0
179+
+ struct.pack('>i', 0) # num_pk_indexes = 0
180+
+ struct.pack('>H', 2) + b'ks' # ksname
181+
+ struct.pack('>H', 2) + b'tb' # cfname
182+
+ struct.pack('>i', 4) # result flags: no_metadata
183+
+ struct.pack('>i', 0) # result colcount = 0
184+
)
185+
186+
features_without_extension = ProtocolFeatures(use_metadata_id=False)
187+
msg = ResultMessage(kind=4)
188+
msg.recv_results_prepared(buf, protocol_version=4,
189+
protocol_features=features_without_extension,
190+
user_type_map={})
191+
assert msg.query_id == b'ab'
192+
assert msg.result_metadata_id is None
193+
194+
def test_recv_results_metadata_changed_flag(self):
195+
"""
196+
When _METADATA_ID_FLAG (0x0008) is set in a ROWS result,
197+
recv_results_metadata must read and store the new result_metadata_id
198+
sent by the server (METADATA_CHANGED signal), and still populate
199+
column_metadata normally.
200+
"""
201+
# Wire layout for a ROWS result with METADATA_CHANGED:
202+
# flags: int(0x0008) = _METADATA_ID_FLAG
203+
# colcount: int(0)
204+
# result_metadata_id: short(4) + b'new1'
205+
# (no columns — colcount=0 — to keep the buffer minimal)
206+
buf = io.BytesIO(
207+
struct.pack('>i', 0x0008) # flags: METADATA_ID_FLAG
208+
+ struct.pack('>i', 0) # colcount = 0
209+
+ struct.pack('>H', 4) + b'new1' # result_metadata_id = b'new1'
210+
)
211+
msg = ResultMessage(kind=RESULT_KIND_ROWS)
212+
msg.recv_results_metadata(buf, user_type_map={})
213+
assert msg.result_metadata_id == b'new1'
214+
assert msg.column_metadata == []
215+
216+
def test_recv_results_metadata_no_metadata_flag_skips_metadata_id(self):
217+
"""
218+
When _NO_METADATA_FLAG (0x0004) is set, recv_results_metadata returns
219+
early and must NOT read or set result_metadata_id, even if the caller
220+
mistakenly sets _METADATA_ID_FLAG alongside it.
221+
"""
222+
# flags = _NO_METADATA_FLAG (0x0004), colcount = 0
223+
buf = io.BytesIO(
224+
struct.pack('>i', 0x0004) # flags: NO_METADATA
225+
+ struct.pack('>i', 0) # colcount = 0
226+
)
227+
msg = ResultMessage(kind=RESULT_KIND_ROWS)
228+
msg.recv_results_metadata(buf, user_type_map={})
229+
assert not hasattr(msg, 'result_metadata_id') or msg.result_metadata_id is None
230+
assert not hasattr(msg, 'column_metadata') or msg.column_metadata is None
231+
71232
def test_query_message(self):
72233
"""
73234
Test to check the appropriate calls are made

tests/unit/test_protocol_features.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,38 @@ class OptionsHolder(object):
2222
assert protocol_features.rate_limit_error == 123
2323
assert protocol_features.shard_id == 0
2424
assert protocol_features.sharding_info is None
25+
26+
def test_use_metadata_id_parsing(self):
27+
"""
28+
Test that SCYLLA_USE_METADATA_ID is parsed from SUPPORTED options.
29+
"""
30+
options = {'SCYLLA_USE_METADATA_ID': ['']}
31+
protocol_features = ProtocolFeatures.parse_from_supported(options)
32+
assert protocol_features.use_metadata_id is True
33+
34+
def test_use_metadata_id_missing(self):
35+
"""
36+
Test that use_metadata_id is False when SCYLLA_USE_METADATA_ID is absent.
37+
"""
38+
options = {'SCYLLA_RATE_LIMIT_ERROR': ['ERROR_CODE=1']}
39+
protocol_features = ProtocolFeatures.parse_from_supported(options)
40+
assert protocol_features.use_metadata_id is False
41+
42+
def test_use_metadata_id_startup_options(self):
43+
"""
44+
Test that SCYLLA_USE_METADATA_ID is included in STARTUP options when negotiated.
45+
"""
46+
options = {'SCYLLA_USE_METADATA_ID': ['']}
47+
protocol_features = ProtocolFeatures.parse_from_supported(options)
48+
startup = {}
49+
protocol_features.add_startup_options(startup)
50+
assert 'SCYLLA_USE_METADATA_ID' in startup
51+
52+
def test_use_metadata_id_not_in_startup_when_not_negotiated(self):
53+
"""
54+
Test that SCYLLA_USE_METADATA_ID is NOT included in STARTUP when not negotiated.
55+
"""
56+
protocol_features = ProtocolFeatures.parse_from_supported({})
57+
startup = {}
58+
protocol_features.add_startup_options(startup)
59+
assert 'SCYLLA_USE_METADATA_ID' not in startup

0 commit comments

Comments
 (0)