@@ -370,19 +370,144 @@ def test_merge_read(con) -> None:
370370
371371
372372def test_handle_COMMAND_COMPLETE_closed_ps (con , mocker ) -> None :
373+ """
374+ Test the handling of prepared statement cache cleanup for different SQL commands.
375+ This test verifies that DDL commands trigger cache cleanup while DML commands preserve the cache.
376+
377+ The test executes the following sequence:
378+ 1. DROP TABLE IF EXISTS t1 (should clear cache)
379+ 2. CREATE TABLE t1 (should clear cache)
380+ 3. ALTER TABLE t1 (should clear cache)
381+ 4. INSERT INTO t1 (should preserve cache)
382+ 5. SELECT FROM t1 (should preserve cache)
383+ 6. ROLLBACK (should clear cache)
384+ 7. CREATE TABLE AS SELECT (should preserve cache)
385+ 8. SELECT FROM t1 (should preserve cache)
386+ 9. DROP TABLE IF EXISTS t1 (should clear cache)
387+
388+ Args:
389+ con: Database connection fixture
390+ mocker: pytest-mock fixture for creating spies
391+ """
373392 with con .cursor () as cursor :
393+ # Create spy to track calls to close_prepared_statement
394+ spy = mocker .spy (con , "close_prepared_statement" )
395+
374396 cursor .execute ("drop table if exists t1" )
397+ assert spy .called
398+ # Two calls expected: one for BEGIN transaction, one for DROP TABLE
399+ assert spy .call_count == 2
400+ spy .reset_mock ()
375401
376- spy = mocker .spy (con , "close_prepared_statement" )
377402 cursor .execute ("create table t1 (a int primary key)" )
403+ assert spy .called
404+ # One call expected for CREATE TABLE
405+ assert spy .call_count == 1
406+ spy .reset_mock ()
378407
379- assert len (con ._caches ) == 1
380- cache_iter = next (iter (con ._caches .values ())) # get first transaction
381- assert len (next (iter (cache_iter .values ()))["statement" ]) == 3 # should be 3 ps in this transaction
382- # begin transaction, drop table t1, create table t1
408+ cursor .execute ("alter table t1 rename column a to b;" )
383409 assert spy .called
410+ # One call expected for ALTER TABLE
411+ assert spy .call_count == 1
412+ spy .reset_mock ()
413+
414+ cursor .execute ("insert into t1 values(1)" )
415+ assert spy .call_count == 0
416+ spy .reset_mock ()
417+
418+ cursor .execute ("select * from t1" )
419+ assert spy .call_count == 0
420+ spy .reset_mock ()
421+
422+ cursor .execute ("rollback" )
423+ assert spy .called
424+ # Three calls expected: INSERT, SELECT, and ROLLBACK statements
384425 assert spy .call_count == 3
426+ spy .reset_mock ()
427+
428+ cursor .execute ("create table t1 as (select 1)" )
429+ assert spy .call_count == 0
430+ spy .reset_mock ()
431+
432+ cursor .execute ("select * from t1" )
433+ assert spy .call_count == 0
434+ spy .reset_mock ()
435+
436+ cursor .execute ("drop table if exists t1" )
437+ assert spy .called
438+ # Four calls expected: BEGIN, CREATE TABLE AS, SELECT, and DROP
439+ assert spy .call_count == 4
440+ spy .reset_mock ()
441+
442+ # Ensure there's exactly one process in the cache
443+ assert len (con ._caches ) == 1
444+ # get cache for current process
445+ cache_iter = next (iter (con ._caches .values ()))
446+
447+ # Verify the number of prepared statements in this transaction
448+ # Should be 7 statements total from all operations
449+ assert len (next (iter (cache_iter .values ()))["statement" ]) == 8 # should be 8 ps in this process
450+
451+ @pytest .mark .parametrize ("test_case" , [
452+ {
453+ "name" : "max_prepared_statements_zero" ,
454+ "max_prepared_statements" : 0 ,
455+ "queries" : ["SELECT 1" , "SELECT 2" ],
456+ "expected_close_calls" : 0 ,
457+ "expected_cache_size" : 0
458+ },
459+ {
460+ "name" : "max_prepared_statements_default" ,
461+ "max_prepared_statements" : 1000 ,
462+ "queries" : ["SELECT 1" , "SELECT 2" ],
463+ "expected_close_calls" : 0 ,
464+ "expected_cache_size" : 3
465+ },
466+ {
467+ "name" : "max_prepared_statements_limit_1" ,
468+ "max_prepared_statements" : 2 ,
469+ "queries" : ["SELECT 1" , "SELECT 2" , "SELECT 3" ],
470+ "expected_close_calls" : 2 ,
471+ "expected_cache_size" : 2
472+ },
473+ {
474+ "name" : "max_prepared_statements_limit_2" ,
475+ "max_prepared_statements" : 2 ,
476+ "queries" : ["SELECT 1" , "SELECT 2" ],
477+ "expected_close_calls" : 2 ,
478+ "expected_cache_size" : 1
479+ }
480+ ])
481+ def test_max_prepared_statement (con , mocker , test_case ) -> None :
482+ """
483+ Test the prepared statement cache management functionality.
484+ This test verifies the behavior of the cache cleanup mechanism when:
485+ 1. max_prepared_statements = 0: No statement will be cached
486+ 2. max_prepared_statements > 0: Statements are cached up to the limit
487+
488+ :param con: Connection object
489+ :param mocker: pytest mocker fixture
490+ :param test_case: Dictionary containing test parameters:
491+ :return: None
492+ """
493+ con .max_prepared_statements = test_case ["max_prepared_statements" ]
494+ with con .cursor () as cursor :
495+ # Create spy to track calls to close_prepared_statement
496+ spy = mocker .spy (con , "close_prepared_statement" )
497+
498+ for query in test_case ["queries" ]:
499+ cursor .execute (query )
500+
501+ # Ensure there's exactly one process in the cache
502+ assert len (con ._caches ) == 1
503+ # Get cache for current process
504+ cache_iter = next (iter (con ._caches .values ()))
505+
506+ # Verify close_prepared_statement was called the expected number of times
507+ assert spy .call_count == test_case ["expected_close_calls" ]
385508
509+ # Verify the final cache size matches expected size
510+ assert len (next (iter (cache_iter .values ()))["ps" ]) == test_case ["expected_cache_size" ]
386511
387512@pytest .mark .parametrize ("_input" , ["NO_SCHEMA_UNIVERSAL_QUERY" , "EXTERNAL_SCHEMA_QUERY" , "LOCAL_SCHEMA_QUERY" ])
388513def test___get_table_filter_clause_return_empty_result (con , _input ) -> None :
0 commit comments