@@ -320,3 +320,122 @@ def test_optimization_efficiency(self):
320320 # The key is we avoid checking 'column_encryption_policy and ...' 200 times
321321 self .assertEqual (mock_policy .contains_column .call_count , 200 ,
322322 "contains_column should be called for each value when policy exists" )
323+
324+
325+ class CythonParserTest (unittest .TestCase ):
326+ """
327+ Tests for the Cython fast-path parsers (ListParser, TupleRowParser)
328+ to verify the column_encryption_policy optimization in obj_parser.pyx.
329+ """
330+
331+ def _build_binary_rows (self , rows ):
332+ """
333+ Build a binary buffer containing encoded rows.
334+
335+ Each row is a list of (size, raw_bytes) pairs.
336+ Prepends a 4-byte big-endian row count.
337+ """
338+ import struct
339+ data = struct .pack ('>i' , len (rows ))
340+ for row in rows :
341+ for raw in row :
342+ if raw is None :
343+ data += struct .pack ('>i' , - 1 ) # NULL
344+ else :
345+ data += struct .pack ('>i' , len (raw )) + raw
346+ return data
347+
348+ def _make_parse_desc (self , column_encryption_policy = None ):
349+ from cassandra .parsing import ParseDesc
350+ from cassandra .deserializers import make_deserializers
351+ from cassandra .policies import ColDesc
352+
353+ colnames = ['col1' , 'col2' ]
354+ coltypes = [Int32Type , UTF8Type ]
355+ coldescs = [ColDesc ('ks' , 'tbl' , 'col1' ), ColDesc ('ks' , 'tbl' , 'col2' )]
356+ deserializers = make_deserializers (coltypes )
357+ return ParseDesc (colnames , coltypes , column_encryption_policy ,
358+ coldescs , deserializers , ProtocolVersion .V4 )
359+
360+ def _int32_bytes (self , val ):
361+ import struct
362+ return struct .pack ('>i' , val )
363+
364+ def test_list_parser_without_encryption (self ):
365+ """ListParser decodes rows correctly without encryption policy."""
366+ from cassandra .bytesio import BytesIOReader
367+ from cassandra .obj_parser import ListParser
368+
369+ desc = self ._make_parse_desc (column_encryption_policy = None )
370+ data = self ._build_binary_rows ([
371+ [self ._int32_bytes (42 ), b'hello' ],
372+ [self ._int32_bytes (100 ), b'world' ],
373+ ])
374+ reader = BytesIOReader (data )
375+ result = ListParser ().parse_rows (reader , desc )
376+
377+ self .assertEqual (len (result ), 2 )
378+ self .assertEqual (result [0 ], (42 , 'hello' ))
379+ self .assertEqual (result [1 ], (100 , 'world' ))
380+
381+ def test_list_parser_with_encryption_no_encrypted_cols (self ):
382+ """ListParser decodes rows correctly when policy exists but no columns are encrypted."""
383+ from cassandra .bytesio import BytesIOReader
384+ from cassandra .obj_parser import ListParser
385+
386+ mock_policy = Mock ()
387+ mock_policy .contains_column = Mock (return_value = False )
388+
389+ desc = self ._make_parse_desc (column_encryption_policy = mock_policy )
390+ data = self ._build_binary_rows ([
391+ [self ._int32_bytes (42 ), b'hello' ],
392+ ])
393+ reader = BytesIOReader (data )
394+ result = ListParser ().parse_rows (reader , desc )
395+
396+ self .assertEqual (len (result ), 1 )
397+ self .assertEqual (result [0 ], (42 , 'hello' ))
398+ # 1 row * 2 columns = 2 calls
399+ self .assertEqual (mock_policy .contains_column .call_count , 2 )
400+
401+ def test_list_parser_with_encrypted_column (self ):
402+ """ListParser decodes rows with an encrypted column (mock decrypt is identity)."""
403+ from cassandra .bytesio import BytesIOReader
404+ from cassandra .obj_parser import ListParser
405+ from cassandra .deserializers import find_deserializer
406+
407+ mock_policy = Mock ()
408+ mock_policy .contains_column = Mock (
409+ side_effect = lambda cd : cd .col == 'col1' )
410+ mock_policy .column_type = Mock (return_value = Int32Type )
411+ # decrypt returns the raw bytes unchanged (identity)
412+ mock_policy .decrypt = Mock (side_effect = lambda cd , val : val )
413+
414+ desc = self ._make_parse_desc (column_encryption_policy = mock_policy )
415+ data = self ._build_binary_rows ([
416+ [self ._int32_bytes (7 ), b'test' ],
417+ ])
418+ reader = BytesIOReader (data )
419+ result = ListParser ().parse_rows (reader , desc )
420+
421+ self .assertEqual (len (result ), 1 )
422+ self .assertEqual (result [0 ], (7 , 'test' ))
423+ self .assertEqual (mock_policy .decrypt .call_count , 1 )
424+ self .assertEqual (mock_policy .column_type .call_count , 1 )
425+
426+ def test_numpy_parser_rejects_encryption (self ):
427+ """NumpyParser raises NotImplementedError when column_encryption_policy is set."""
428+ try :
429+ from cassandra .numpy_parser import NumpyParser
430+ except ImportError :
431+ self .skipTest ("NumPy or numpy_parser not available" )
432+
433+ from cassandra .bytesio import BytesIOReader
434+
435+ mock_policy = Mock ()
436+ desc = self ._make_parse_desc (column_encryption_policy = mock_policy )
437+ data = self ._build_binary_rows ([[self ._int32_bytes (1 ), b'x' ]])
438+ reader = BytesIOReader (data )
439+
440+ with self .assertRaises (NotImplementedError ):
441+ NumpyParser ().parse_rows (reader , desc )
0 commit comments