1212# pylint: disable=too-many-lines # Large file due to comprehensive DB-API 2.0 implementation
1313
1414import decimal
15+ import logging
1516import uuid
1617import datetime
1718import warnings
@@ -140,6 +141,7 @@ def __init__(self, connection: "Connection", timeout: int = 0) -> None:
140141 False
141142 ] # Indicates if last_executed_stmt was prepared by ddbc shim.
142143 # Is a list instead of a bool coz bools in Python are immutable.
144+ self ._cached_param_types = None # Cached ParamInfo list for prepare-cache reuse
143145
144146 # Initialize attributes that may be defined later to avoid pylint warnings
145147 # Note: _original_fetch* methods are not initialized here as they need to be
@@ -747,6 +749,18 @@ def _reset_cursor(self) -> None:
747749
748750 # Reinitialize the statement handle
749751 self ._initialize_cursor ()
752+ self .is_stmt_prepared = [False ]
753+ self ._cached_param_types = None
754+
755+ def _soft_reset_cursor (self ) -> None :
756+ """Lightweight reset: close cursor and unbind params without freeing the HSTMT.
757+
758+ Preserves the prepared statement plan on the server so repeated
759+ executions of the same SQL skip SQLPrepare entirely.
760+ """
761+ if self .hstmt :
762+ ddbc_bindings .DDBCSQLResetStmt (self .hstmt )
763+ self ._clear_rownumber ()
750764
751765 def close (self ) -> None :
752766 """
@@ -1349,8 +1363,10 @@ def execute( # pylint: disable=too-many-locals,too-many-branches,too-many-state
13491363
13501364 self ._check_closed () # Check if the cursor is closed
13511365 if reset_cursor :
1352- logger .debug ("execute: Resetting cursor state" )
1353- self ._reset_cursor ()
1366+ if self .hstmt :
1367+ self ._soft_reset_cursor ()
1368+ else :
1369+ self ._reset_cursor ()
13541370
13551371 # Clear any previous messages
13561372 self .messages = []
@@ -1385,20 +1401,28 @@ def execute( # pylint: disable=too-many-locals,too-many-branches,too-many-state
13851401 # Check if single parameter is a nested container that should be unwrapped
13861402 # e.g., execute("SELECT ?", (value,)) vs execute("SELECT ?, ?", ((1, 2),))
13871403 if isinstance (parameters , tuple ) and len (parameters ) == 1 :
1388- # Could be either (value,) for single param or ((tuple),) for nested
1389- # Check if it's a nested container
13901404 if isinstance (parameters [0 ], (tuple , list , dict )):
13911405 actual_params = parameters [0 ]
13921406 else :
13931407 actual_params = parameters
13941408 else :
13951409 actual_params = parameters
13961410
1397- # Convert parameters based on detected style
1398- operation , converted_params = detect_and_convert_parameters (operation , actual_params )
1399-
1400- # Convert back to list format expected by the binding code
1401- parameters = list (converted_params )
1411+ # Skip detect_and_convert_parameters when re-executing the same SQL —
1412+ # the parameter style (qmark vs pyformat) won't change between calls.
1413+ if operation == self .last_executed_stmt and isinstance (
1414+ actual_params , (tuple , list )
1415+ ):
1416+ parameters = (
1417+ list (actual_params )
1418+ if not isinstance (actual_params , list )
1419+ else actual_params
1420+ )
1421+ else :
1422+ operation , converted_params = detect_and_convert_parameters (
1423+ operation , actual_params
1424+ )
1425+ parameters = list (converted_params )
14021426 else :
14031427 parameters = []
14041428
@@ -1426,35 +1450,36 @@ def execute( # pylint: disable=too-many-locals,too-many-branches,too-many-state
14261450 paraminfo = self ._create_parameter_types_list (param , param_info , parameters , i )
14271451 parameters_type .append (paraminfo )
14281452
1429- # TODO: Use a more sophisticated string compare that handles redundant spaces etc.
1430- # Also consider storing last query's hash instead of full query string. This will help
1431- # in low-memory conditions
1432- # (Ex: huge number of parallel queries with huge query string sizes)
1433- if operation != self .last_executed_stmt :
1434- # Executing a new statement. Reset is_stmt_prepared to false
1453+ # Prepare caching: skip SQLPrepare when re-executing the same SQL
1454+ # with parameters. The HSTMT is reused via _soft_reset_cursor, so the
1455+ # server-side plan from the previous SQLPrepare is still valid.
1456+ same_sql = parameters and operation == self .last_executed_stmt and self .is_stmt_prepared [0 ]
1457+ if not same_sql :
14351458 self .is_stmt_prepared = [False ]
1459+ effective_use_prepare = use_prepare and not same_sql
14361460
1437- for i , param in enumerate (parameters ):
1438- logger .debug (
1439- """Parameter number: %s, Parameter: %s,
1440- Param Python Type: %s, ParamInfo: %s, %s, %s, %s, %s""" ,
1441- i + 1 ,
1442- param ,
1443- str (type (param )),
1444- parameters_type [i ].paramSQLType ,
1445- parameters_type [i ].paramCType ,
1446- parameters_type [i ].columnSize ,
1447- parameters_type [i ].decimalDigits ,
1448- parameters_type [i ].inputOutputType ,
1449- )
1461+ if logger .isEnabledFor (logging .DEBUG ):
1462+ for i , param in enumerate (parameters ):
1463+ logger .debug (
1464+ """Parameter number: %s, Parameter: %s,
1465+ Param Python Type: %s, ParamInfo: %s, %s, %s, %s, %s""" ,
1466+ i + 1 ,
1467+ param ,
1468+ str (type (param )),
1469+ parameters_type [i ].paramSQLType ,
1470+ parameters_type [i ].paramCType ,
1471+ parameters_type [i ].columnSize ,
1472+ parameters_type [i ].decimalDigits ,
1473+ parameters_type [i ].inputOutputType ,
1474+ )
14501475
14511476 ret = ddbc_bindings .DDBCSQLExecute (
14521477 self .hstmt ,
14531478 operation ,
14541479 parameters ,
14551480 parameters_type ,
14561481 self .is_stmt_prepared ,
1457- use_prepare ,
1482+ effective_use_prepare ,
14581483 encoding_settings ,
14591484 )
14601485 # Check return code
@@ -1467,8 +1492,8 @@ def execute( # pylint: disable=too-many-locals,too-many-branches,too-many-state
14671492 self ._reset_cursor ()
14681493 raise
14691494
1470- # Capture any diagnostic messages (SQL_SUCCESS_WITH_INFO, etc.)
1471- if self .hstmt :
1495+ # Capture diagnostic messages only when the driver signalled info.
1496+ if ret == ddbc_sql_const . SQL_SUCCESS_WITH_INFO . value and self .hstmt :
14721497 self .messages .extend (ddbc_bindings .DDBCSQLGetAllDiagRecords (self .hstmt ))
14731498
14741499 self .last_executed_stmt = operation
0 commit comments