@@ -370,19 +370,144 @@ def test_merge_read(con) -> None:
370
370
371
371
372
372
def 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
+ """
373
392
with con .cursor () as cursor :
393
+ # Create spy to track calls to close_prepared_statement
394
+ spy = mocker .spy (con , "close_prepared_statement" )
395
+
374
396
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 ()
375
401
376
- spy = mocker .spy (con , "close_prepared_statement" )
377
402
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 ()
378
407
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;" )
383
409
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
384
425
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" ]
385
508
509
+ # Verify the final cache size matches expected size
510
+ assert len (next (iter (cache_iter .values ()))["ps" ]) == test_case ["expected_cache_size" ]
386
511
387
512
@pytest .mark .parametrize ("_input" , ["NO_SCHEMA_UNIVERSAL_QUERY" , "EXTERNAL_SCHEMA_QUERY" , "LOCAL_SCHEMA_QUERY" ])
388
513
def test___get_table_filter_clause_return_empty_result (con , _input ) -> None :
0 commit comments