From c635707332a32149244cb967409e9ede3c9aeea9 Mon Sep 17 00:00:00 2001 From: Jahnvi Thakkar Date: Wed, 3 Sep 2025 12:30:31 +0530 Subject: [PATCH 01/20] FIX: Python Code linting --- mssql_python/__init__.py | 24 +- mssql_python/auth.py | 58 ++-- mssql_python/bcp_options.py | 24 +- mssql_python/connection.py | 352 +++++++++++-------- mssql_python/constants.py | 5 +- mssql_python/cursor.py | 479 +++++++++++++++----------- mssql_python/db_connection.py | 13 +- mssql_python/ddbc_bindings.py | 82 +++-- mssql_python/exceptions.py | 311 ++++++----------- mssql_python/helpers.py | 24 +- mssql_python/logging_config.py | 70 ++-- mssql_python/mssql_python.pyi | 10 +- mssql_python/pooling.py | 43 ++- mssql_python/row.py | 36 +- mssql_python/testing_ddbc_bindings.py | 43 ++- mssql_python/type.py | 26 +- 16 files changed, 884 insertions(+), 716 deletions(-) diff --git a/mssql_python/__init__.py b/mssql_python/__init__.py index 07113646..a040c084 100644 --- a/mssql_python/__init__.py +++ b/mssql_python/__init__.py @@ -37,6 +37,7 @@ # Connection Objects from .db_connection import connect, Connection +from .pooling import PoolingManager # Cursor Objects from .cursor import Cursor @@ -58,20 +59,19 @@ paramstyle = "qmark" threadsafety = 1 -from .pooling import PoolingManager def pooling(max_size=100, idle_timeout=600, enabled=True): -# """ -# Enable connection pooling with the specified parameters. -# By default: -# - If not explicitly called, pooling will be auto-enabled with default values. + """ + Enable connection pooling with the specified parameters. + By default: + - If not explicitly called, pooling will be auto-enabled with default values. + + Args: + max_size (int): Maximum number of connections in the pool. + idle_timeout (int): Time in seconds before idle connections are closed. -# Args: -# max_size (int): Maximum number of connections in the pool. -# idle_timeout (int): Time in seconds before idle connections are closed. - -# Returns: -# None -# """ + Returns: + None + """ if not enabled: PoolingManager.disable() else: diff --git a/mssql_python/auth.py b/mssql_python/auth.py index c7e6683a..56491cea 100644 --- a/mssql_python/auth.py +++ b/mssql_python/auth.py @@ -6,12 +6,13 @@ import platform import struct -from typing import Tuple, Dict, Optional, Union +from typing import Tuple, Dict, Optional from mssql_python.constants import AuthType + class AADAuth: """Handles Azure Active Directory authentication""" - + @staticmethod def get_token_struct(token: str) -> bytes: """Convert token to SQL Server compatible format""" @@ -22,21 +23,21 @@ def get_token_struct(token: str) -> bytes: def get_token(auth_type: str) -> bytes: """Get token using the specified authentication type""" from azure.identity import ( - DefaultAzureCredential, - DeviceCodeCredential, - InteractiveBrowserCredential + DefaultAzureCredential, + DeviceCodeCredential, + InteractiveBrowserCredential, ) from azure.core.exceptions import ClientAuthenticationError - + # Mapping of auth types to credential classes credential_map = { "default": DefaultAzureCredential, "devicecode": DeviceCodeCredential, "interactive": InteractiveBrowserCredential, } - + credential_class = credential_map[auth_type] - + try: credential = credential_class() token = credential.get_token("https://database.windows.net/.default").token @@ -50,18 +51,21 @@ def get_token(auth_type: str) -> bytes: ) from e except Exception as e: # Catch any other unexpected exceptions - raise RuntimeError(f"Failed to create {credential_class.__name__}: {e}") from e + raise RuntimeError( + f"Failed to create {credential_class.__name__}: {e}" + ) from e + def process_auth_parameters(parameters: list) -> Tuple[list, Optional[str]]: """ Process connection parameters and extract authentication type. - + Args: parameters: List of connection string parameters - + Returns: Tuple[list, Optional[str]]: Modified parameters and authentication type - + Raises: ValueError: If an invalid authentication type is provided """ @@ -88,7 +92,7 @@ def process_auth_parameters(parameters: list) -> Tuple[list, Optional[str]]: # Interactive authentication (browser-based); only append parameter for non-Windows if platform.system().lower() == "windows": auth_type = None # Let Windows handle AADInteractive natively - + elif value_lower == AuthType.DEVICE_CODE.value: # Device code authentication (for devices without browser) auth_type = "devicecode" @@ -99,40 +103,48 @@ def process_auth_parameters(parameters: list) -> Tuple[list, Optional[str]]: return modified_parameters, auth_type + def remove_sensitive_params(parameters: list) -> list: """Remove sensitive parameters from connection string""" exclude_keys = [ - "uid=", "pwd=", "encrypt=", "trustservercertificate=", "authentication=" + "uid=", + "pwd=", + "encrypt=", + "trustservercertificate=", + "authentication=", ] return [ - param for param in parameters + param + for param in parameters if not any(param.lower().startswith(exclude) for exclude in exclude_keys) ] + def get_auth_token(auth_type: str) -> Optional[bytes]: """Get authentication token based on auth type""" if not auth_type: return None - + # Handle platform-specific logic for interactive auth if auth_type == "interactive" and platform.system().lower() == "windows": return None # Let Windows handle AADInteractive natively - + try: return AADAuth.get_token(auth_type) except (ValueError, RuntimeError): return None + def process_connection_string(connection_string: str) -> Tuple[str, Optional[Dict]]: """ Process connection string and handle authentication. - + Args: connection_string: The connection string to process - + Returns: Tuple[str, Optional[Dict]]: Processed connection string and attrs_before dict if needed - + Raises: ValueError: If the connection string is invalid or empty """ @@ -145,9 +157,9 @@ def process_connection_string(connection_string: str) -> Tuple[str, Optional[Dic raise ValueError("Connection string cannot be empty") parameters = connection_string.split(";") - + # Validate that there's at least one valid parameter - if not any('=' in param for param in parameters): + if not any("=" in param for param in parameters): raise ValueError("Invalid connection string format") modified_parameters, auth_type = process_auth_parameters(parameters) @@ -158,4 +170,4 @@ def process_connection_string(connection_string: str) -> Tuple[str, Optional[Dic if token_struct: return ";".join(modified_parameters) + ";", {1256: token_struct} - return ";".join(modified_parameters) + ";", None \ No newline at end of file + return ";".join(modified_parameters) + ";", None diff --git a/mssql_python/bcp_options.py b/mssql_python/bcp_options.py index 7dab82d5..9799af6d 100644 --- a/mssql_python/bcp_options.py +++ b/mssql_python/bcp_options.py @@ -1,3 +1,9 @@ +""" +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. +This module provides options for bulk copy operations. +""" + from dataclasses import dataclass, field from typing import List, Optional, Literal @@ -31,6 +37,9 @@ class ColumnFormat: file_col: int = 1 def __post_init__(self): + """ + Validate column format options. + """ if self.prefix_len < 0: raise ValueError("prefix_len must be a non-negative integer.") if self.data_len < 0: @@ -88,12 +97,21 @@ class BCPOptions: columns: List[ColumnFormat] = field(default_factory=list) def __post_init__(self): + """ + Validate BCP options. + """ if self.direction not in ["in", "out"]: raise ValueError("direction must be 'in' or 'out'.") if not self.data_file: - raise ValueError("data_file must be provided and non-empty for 'in' or 'out' directions.") - if self.error_file is None or not self.error_file: # Making error_file mandatory for in/out - raise ValueError("error_file must be provided and non-empty for 'in' or 'out' directions.") + raise ValueError( + "data_file must be provided and non-empty for 'in' or 'out' directions." + ) + if ( + self.error_file is None or not self.error_file + ): # Making error_file mandatory for in/out + raise ValueError( + "error_file must be provided and non-empty for 'in' or 'out' directions." + ) if self.format_file is not None and not self.format_file: raise ValueError("format_file, if provided, must not be an empty string.") diff --git a/mssql_python/connection.py b/mssql_python/connection.py index d0a88fb5..c92dc08e 100644 --- a/mssql_python/connection.py +++ b/mssql_python/connection.py @@ -2,7 +2,7 @@ Copyright (c) Microsoft Corporation. Licensed under the MIT license. This module defines the Connection class, which is used to manage a connection to a database. -The class provides methods to establish a connection, create cursors, commit transactions, +The class provides methods to establish a connection, create cursors, commit transactions, roll back transactions, and close the connection. Resource Management: - All cursors created from this connection are tracked internally. @@ -10,37 +10,53 @@ - Do not use any cursor after the connection is closed; doing so will raise an exception. - Cursors are also cleaned up automatically when no longer referenced, to prevent memory leaks. """ + import weakref import re import codecs from mssql_python.cursor import Cursor -from mssql_python.helpers import add_driver_to_connection_str, sanitize_connection_string, sanitize_user_input, log +from mssql_python.helpers import ( + add_driver_to_connection_str, + sanitize_connection_string, + sanitize_user_input, + log, +) from mssql_python import ddbc_bindings from mssql_python.pooling import PoolingManager -from mssql_python.exceptions import InterfaceError, ProgrammingError from mssql_python.auth import process_connection_string from mssql_python.constants import ConstantsDDBC +# Import all DB-API 2.0 exception classes for Connection attributes +from mssql_python.exceptions import ( + Warning, + Error, + InterfaceError, + DatabaseError, + DataError, + OperationalError, + IntegrityError, + InternalError, + ProgrammingError, + NotSupportedError, +) + # Add SQL_WMETADATA constant for metadata decoding configuration SQL_WMETADATA = -99 # Special flag for column name decoding # UTF-16 encoding variants that should use SQL_WCHAR by default -UTF16_ENCODINGS = frozenset([ - 'utf-16', - 'utf-16le', - 'utf-16be' -]) +UTF16_ENCODINGS = frozenset(["utf-16", "utf-16le", "utf-16be"]) + def _validate_encoding(encoding: str) -> bool: """ Cached encoding validation using codecs.lookup(). - + Args: encoding (str): The encoding name to validate. - + Returns: bool: True if encoding is valid, False otherwise. - + Note: Uses LRU cache to avoid repeated expensive codecs.lookup() calls. Cache size is limited to 128 entries which should cover most use cases. @@ -51,20 +67,6 @@ def _validate_encoding(encoding: str) -> bool: except LookupError: return False -# Import all DB-API 2.0 exception classes for Connection attributes -from mssql_python.exceptions import ( - Warning, - Error, - InterfaceError, - DatabaseError, - DataError, - OperationalError, - IntegrityError, - InternalError, - ProgrammingError, - NotSupportedError, -) - class Connection: """ @@ -84,7 +86,7 @@ class Connection: cursor = conn.cursor() cursor.execute("INSERT INTO table VALUES (?)", [value]) # Connection is automatically closed when exiting the with block - + For long-lived connections, use without context manager: conn = connect(connection_string) try: @@ -107,7 +109,8 @@ class Connection: """ # DB-API 2.0 Exception attributes - # These allow users to catch exceptions using connection.Error, connection.ProgrammingError, etc. + # These allow users to catch exceptions using connection.Error, + # connection.ProgrammingError, etc. Warning = Warning Error = Error InterfaceError = InterfaceError @@ -119,7 +122,13 @@ class Connection: ProgrammingError = ProgrammingError NotSupportedError = NotSupportedError - def __init__(self, connection_str: str = "", autocommit: bool = False, attrs_before: dict = None, **kwargs) -> None: + def __init__( + self, + connection_str: str = "", + autocommit: bool = False, + attrs_before: dict = None, + **kwargs, + ) -> None: """ Initialize the connection object with the specified connection string and parameters. @@ -136,7 +145,7 @@ def __init__(self, connection_str: str = "", autocommit: bool = False, attrs_bef ValueError: If the connection string is invalid or connection fails. This method sets up the initial state for the connection object, - preparing it for further operations such as connecting to the + preparing it for further operations such as connecting to the database, executing queries, etc. """ self.connection_str = self._construct_connection_string( @@ -147,24 +156,24 @@ def __init__(self, connection_str: str = "", autocommit: bool = False, attrs_bef # Initialize encoding settings with defaults for Python 3 # Python 3 only has str (which is Unicode), so we use utf-16le by default self._encoding_settings = { - 'encoding': 'utf-16le', - 'ctype': ConstantsDDBC.SQL_WCHAR.value + "encoding": "utf-16le", + "ctype": ConstantsDDBC.SQL_WCHAR.value, } # Initialize decoding settings with Python 3 defaults self._decoding_settings = { ConstantsDDBC.SQL_CHAR.value: { - 'encoding': 'utf-8', - 'ctype': ConstantsDDBC.SQL_CHAR.value + "encoding": "utf-8", + "ctype": ConstantsDDBC.SQL_CHAR.value, }, ConstantsDDBC.SQL_WCHAR.value: { - 'encoding': 'utf-16le', - 'ctype': ConstantsDDBC.SQL_WCHAR.value + "encoding": "utf-16le", + "ctype": ConstantsDDBC.SQL_WCHAR.value, }, SQL_WMETADATA: { - 'encoding': 'utf-16le', - 'ctype': ConstantsDDBC.SQL_WCHAR.value - } + "encoding": "utf-16le", + "ctype": ConstantsDDBC.SQL_WCHAR.value, + }, } # Check if the connection string contains authentication parameters @@ -176,13 +185,15 @@ def __init__(self, connection_str: str = "", autocommit: bool = False, attrs_bef self.connection_str = connection_result[0] if connection_result[1]: self._attrs_before.update(connection_result[1]) - + self._closed = False - + # Using WeakSet which automatically removes cursors when they are no longer in use # It is a set that holds weak references to its elements. - # When an object is only weakly referenced, it can be garbage collected even if it's still in the set. - # It prevents memory leaks by ensuring that cursors are cleaned up when no longer in use without requiring explicit deletion. + # When an object is only weakly referenced, it can be + # garbage collected even if it's still in the set. + # It prevents memory leaks by ensuring that cursors are + # cleaned up when no longer in use without requiring explicit deletion. # TODO: Think and implement scenarios for multi-threaded access to cursors self._cursors = weakref.WeakSet() @@ -190,12 +201,14 @@ def __init__(self, connection_str: str = "", autocommit: bool = False, attrs_bef if not PoolingManager.is_initialized(): PoolingManager.enable() self._pooling = PoolingManager.is_enabled() - self._conn = ddbc_bindings.Connection(self.connection_str, self._pooling, self._attrs_before) + self._conn = ddbc_bindings.Connection( + self.connection_str, self._pooling, self._attrs_before + ) self.setautocommit(autocommit) def _construct_connection_string(self, connection_str: str = "", **kwargs) -> str: """ - Construct the connection string by concatenating the connection string + Construct the connection string by concatenating the connection string with key/value pairs from kwargs. Args: @@ -226,10 +239,10 @@ def _construct_connection_string(self, connection_str: str = "", **kwargs) -> st continue conn_str += f"{key}={value};" - log('info', "Final connection string: %s", sanitize_connection_string(conn_str)) + log("info", "Final connection string: %s", sanitize_connection_string(conn_str)) return conn_str - + @property def autocommit(self) -> bool: """ @@ -249,7 +262,7 @@ def autocommit(self, value: bool) -> None: None """ self.setautocommit(value) - log('info', "Autocommit mode set to %s.", value) + log("info", "Autocommit mode set to %s.", value) def setautocommit(self, value: bool = False) -> None: """ @@ -266,28 +279,29 @@ def setautocommit(self, value: bool = False) -> None: def setencoding(self, encoding=None, ctype=None): """ Sets the text encoding for SQL statements and text parameters. - + Since Python 3 only has str (which is Unicode), this method configures how text is encoded when sending to the database. - + Args: - encoding (str, optional): The encoding to use. This must be a valid Python + encoding (str, optional): The encoding to use. This must be a valid Python encoding that converts text to bytes. If None, defaults to 'utf-16le'. - ctype (int, optional): The C data type to use when passing data: - SQL_CHAR or SQL_WCHAR. If not provided, SQL_WCHAR is used for - UTF-16 variants (see UTF16_ENCODINGS constant). SQL_CHAR is used for all other encodings. - + ctype (int, optional): The C data type to use when passing data: + SQL_CHAR or SQL_WCHAR. If not provided, SQL_WCHAR is used for + UTF-16 variants (see UTF16_ENCODINGS constant). + SQL_CHAR is used for all other encodings. + Returns: None - + Raises: ProgrammingError: If the encoding is not valid or not supported. InterfaceError: If the connection is closed. - + Example: # For databases that only communicate with UTF-8 cnxn.setencoding(encoding='utf-8') - + # For explicitly using SQL_CHAR cnxn.setencoding(encoding='utf-8', ctype=mssql_python.SQL_CHAR) """ @@ -296,60 +310,72 @@ def setencoding(self, encoding=None, ctype=None): driver_error="Connection is closed", ddbc_error="Connection is closed", ) - + # Set default encoding if not provided if encoding is None: - encoding = 'utf-16le' - + encoding = "utf-16le" + # Validate encoding using cached validation for better performance if not _validate_encoding(encoding): # Log the sanitized encoding for security - log('warning', "Invalid encoding attempted: %s", sanitize_user_input(str(encoding))) + log( + "warning", + "Invalid encoding attempted: %s", + sanitize_user_input(str(encoding)), + ) raise ProgrammingError( driver_error=f"Unsupported encoding: {encoding}", ddbc_error=f"The encoding '{encoding}' is not supported by Python", ) - + # Normalize encoding to casefold for more robust Unicode handling encoding = encoding.casefold() - + # Set default ctype based on encoding if not provided if ctype is None: if encoding in UTF16_ENCODINGS: ctype = ConstantsDDBC.SQL_WCHAR.value else: ctype = ConstantsDDBC.SQL_CHAR.value - + # Validate ctype valid_ctypes = [ConstantsDDBC.SQL_CHAR.value, ConstantsDDBC.SQL_WCHAR.value] if ctype not in valid_ctypes: - # Log the sanitized ctype for security - log('warning', "Invalid ctype attempted: %s", sanitize_user_input(str(ctype))) + # Log the sanitized ctype for security + log( + "warning", + "Invalid ctype attempted: %s", + sanitize_user_input(str(ctype)), + ) raise ProgrammingError( driver_error=f"Invalid ctype: {ctype}", - ddbc_error=f"ctype must be SQL_CHAR ({ConstantsDDBC.SQL_CHAR.value}) or SQL_WCHAR ({ConstantsDDBC.SQL_WCHAR.value})", + ddbc_error=( + f"ctype must be SQL_CHAR ({ConstantsDDBC.SQL_CHAR.value}) or " + f"SQL_WCHAR ({ConstantsDDBC.SQL_WCHAR.value})" + ), ) - + # Store the encoding settings - self._encoding_settings = { - 'encoding': encoding, - 'ctype': ctype - } - + self._encoding_settings = {"encoding": encoding, "ctype": ctype} + # Log with sanitized values for security - log('info', "Text encoding set to %s with ctype %s", - sanitize_user_input(encoding), sanitize_user_input(str(ctype))) + log( + "info", + "Text encoding set to %s with ctype %s", + sanitize_user_input(encoding), + sanitize_user_input(str(ctype)), + ) def getencoding(self): """ Gets the current text encoding settings. - + Returns: dict: A dictionary containing 'encoding' and 'ctype' keys. - + Raises: InterfaceError: If the connection is closed. - + Example: settings = cnxn.getencoding() print(f"Current encoding: {settings['encoding']}") @@ -360,125 +386,149 @@ def getencoding(self): driver_error="Connection is closed", ddbc_error="Connection is closed", ) - + return self._encoding_settings.copy() def setdecoding(self, sqltype, encoding=None, ctype=None): """ Sets the text decoding used when reading SQL_CHAR and SQL_WCHAR from the database. - + This method configures how text data is decoded when reading from the database. In Python 3, all text is Unicode (str), so this primarily affects the encoding used to decode bytes from the database. - + Args: sqltype (int): The SQL type being configured: SQL_CHAR, SQL_WCHAR, or SQL_WMETADATA. SQL_WMETADATA is a special flag for configuring column name decoding. encoding (str, optional): The Python encoding to use when decoding the data. If None, uses default encoding based on sqltype. - ctype (int, optional): The C data type to request from SQLGetData: + ctype (int, optional): The C data type to request from SQLGetData: SQL_CHAR or SQL_WCHAR. If None, uses default based on encoding. - + Returns: None - + Raises: ProgrammingError: If the sqltype, encoding, or ctype is invalid. InterfaceError: If the connection is closed. - + Example: # Configure SQL_CHAR to use UTF-8 decoding cnxn.setdecoding(mssql_python.SQL_CHAR, encoding='utf-8') - + # Configure column metadata decoding cnxn.setdecoding(mssql_python.SQL_WMETADATA, encoding='utf-16le') - + # Use explicit ctype - cnxn.setdecoding(mssql_python.SQL_WCHAR, encoding='utf-16le', ctype=mssql_python.SQL_WCHAR) + cnxn.setdecoding( + mssql_python.SQL_WCHAR, encoding='utf-16le', + ctype=mssql_python.SQL_WCHAR + ) """ if self._closed: raise InterfaceError( driver_error="Connection is closed", ddbc_error="Connection is closed", ) - + # Validate sqltype valid_sqltypes = [ ConstantsDDBC.SQL_CHAR.value, ConstantsDDBC.SQL_WCHAR.value, - SQL_WMETADATA + SQL_WMETADATA, ] if sqltype not in valid_sqltypes: - log('warning', "Invalid sqltype attempted: %s", sanitize_user_input(str(sqltype))) + log( + "warning", + "Invalid sqltype attempted: %s", + sanitize_user_input(str(sqltype)), + ) raise ProgrammingError( driver_error=f"Invalid sqltype: {sqltype}", - ddbc_error=f"sqltype must be SQL_CHAR ({ConstantsDDBC.SQL_CHAR.value}), SQL_WCHAR ({ConstantsDDBC.SQL_WCHAR.value}), or SQL_WMETADATA ({SQL_WMETADATA})", + ddbc_error=( + f"sqltype must be SQL_CHAR ({ConstantsDDBC.SQL_CHAR.value}), " + f"SQL_WCHAR ({ConstantsDDBC.SQL_WCHAR.value}), " + f"or SQL_WMETADATA ({SQL_WMETADATA})" + ), ) - + # Set default encoding based on sqltype if not provided if encoding is None: if sqltype == ConstantsDDBC.SQL_CHAR.value: - encoding = 'utf-8' # Default for SQL_CHAR in Python 3 + encoding = "utf-8" # Default for SQL_CHAR in Python 3 else: # SQL_WCHAR or SQL_WMETADATA - encoding = 'utf-16le' # Default for SQL_WCHAR in Python 3 - + encoding = "utf-16le" # Default for SQL_WCHAR in Python 3 + # Validate encoding using cached validation for better performance if not _validate_encoding(encoding): - log('warning', "Invalid encoding attempted: %s", sanitize_user_input(str(encoding))) + log( + "warning", + "Invalid encoding attempted: %s", + sanitize_user_input(str(encoding)), + ) raise ProgrammingError( driver_error=f"Unsupported encoding: {encoding}", ddbc_error=f"The encoding '{encoding}' is not supported by Python", ) - + # Normalize encoding to lowercase for consistency encoding = encoding.lower() - + # Set default ctype based on encoding if not provided if ctype is None: if encoding in UTF16_ENCODINGS: ctype = ConstantsDDBC.SQL_WCHAR.value else: ctype = ConstantsDDBC.SQL_CHAR.value - + # Validate ctype valid_ctypes = [ConstantsDDBC.SQL_CHAR.value, ConstantsDDBC.SQL_WCHAR.value] if ctype not in valid_ctypes: - log('warning', "Invalid ctype attempted: %s", sanitize_user_input(str(ctype))) + log( + "warning", + "Invalid ctype attempted: %s", + sanitize_user_input(str(ctype)), + ) raise ProgrammingError( driver_error=f"Invalid ctype: {ctype}", - ddbc_error=f"ctype must be SQL_CHAR ({ConstantsDDBC.SQL_CHAR.value}) or SQL_WCHAR ({ConstantsDDBC.SQL_WCHAR.value})", + ddbc_error=( + f"ctype must be SQL_CHAR ({ConstantsDDBC.SQL_CHAR.value}) " + f"or SQL_WCHAR ({ConstantsDDBC.SQL_WCHAR.value})" + ), ) - + # Store the decoding settings for the specified sqltype - self._decoding_settings[sqltype] = { - 'encoding': encoding, - 'ctype': ctype - } - + self._decoding_settings[sqltype] = {"encoding": encoding, "ctype": ctype} + # Log with sanitized values for security sqltype_name = { ConstantsDDBC.SQL_CHAR.value: "SQL_CHAR", - ConstantsDDBC.SQL_WCHAR.value: "SQL_WCHAR", - SQL_WMETADATA: "SQL_WMETADATA" + ConstantsDDBC.SQL_WCHAR.value: "SQL_WCHAR", + SQL_WMETADATA: "SQL_WMETADATA", }.get(sqltype, str(sqltype)) - - log('info', "Text decoding set for %s to %s with ctype %s", - sqltype_name, sanitize_user_input(encoding), sanitize_user_input(str(ctype))) + + log( + "info", + "Text decoding set for %s to %s with ctype %s", + sqltype_name, + sanitize_user_input(encoding), + sanitize_user_input(str(ctype)), + ) def getdecoding(self, sqltype): """ Gets the current text decoding settings for the specified SQL type. - + Args: sqltype (int): The SQL type to get settings for: SQL_CHAR, SQL_WCHAR, or SQL_WMETADATA. - + Returns: dict: A dictionary containing 'encoding' and 'ctype' keys for the specified sqltype. - + Raises: ProgrammingError: If the sqltype is invalid. InterfaceError: If the connection is closed. - + Example: settings = cnxn.getdecoding(mssql_python.SQL_CHAR) print(f"SQL_CHAR encoding: {settings['encoding']}") @@ -489,19 +539,23 @@ def getdecoding(self, sqltype): driver_error="Connection is closed", ddbc_error="Connection is closed", ) - + # Validate sqltype valid_sqltypes = [ ConstantsDDBC.SQL_CHAR.value, ConstantsDDBC.SQL_WCHAR.value, - SQL_WMETADATA + SQL_WMETADATA, ] if sqltype not in valid_sqltypes: raise ProgrammingError( driver_error=f"Invalid sqltype: {sqltype}", - ddbc_error=f"sqltype must be SQL_CHAR ({ConstantsDDBC.SQL_CHAR.value}), SQL_WCHAR ({ConstantsDDBC.SQL_WCHAR.value}), or SQL_WMETADATA ({SQL_WMETADATA})", + ddbc_error=( + f"sqltype must be SQL_CHAR ({ConstantsDDBC.SQL_CHAR.value}), " + f" SQL_WCHAR ({ConstantsDDBC.SQL_WCHAR.value}), " + f"or SQL_WMETADATA ({SQL_WMETADATA})" + ) ) - + return self._decoding_settings[sqltype].copy() def cursor(self) -> Cursor: @@ -518,8 +572,8 @@ def cursor(self) -> Cursor: Raises: DatabaseError: If there is an error while creating the cursor. InterfaceError: If there is an error related to the database interface. + """ - """Return a new Cursor object using the connection.""" if self._closed: # raise InterfaceError raise InterfaceError( @@ -545,7 +599,7 @@ def commit(self) -> None: """ # Commit the current transaction self._conn.commit() - log('info', "Transaction committed successfully.") + log("info", "Transaction committed successfully.") def rollback(self) -> None: """ @@ -560,7 +614,7 @@ def rollback(self) -> None: """ # Roll back the current transaction self._conn.rollback() - log('info', "Transaction rolled back successfully.") + log("info", "Transaction rolled back successfully.") def close(self) -> None: """ @@ -578,25 +632,28 @@ def close(self) -> None: # Close the connection if self._closed: return - + # Close all cursors first, but don't let one failure stop the others - if hasattr(self, '_cursors'): + if hasattr(self, "_cursors"): # Convert to list to avoid modification during iteration cursors_to_close = list(self._cursors) close_errors = [] - + for cursor in cursors_to_close: try: if not cursor.closed: cursor.close() - except Exception as e: + except (InterfaceError, ProgrammingError, DatabaseError) as e: # Collect errors but continue closing other cursors close_errors.append(f"Error closing cursor: {e}") - log('warning', f"Error closing cursor: {e}") - + log("warning", f"Error closing cursor: {e}") + # If there were errors closing cursors, log them but continue if close_errors: - log('warning', f"Encountered {len(close_errors)} errors while closing cursors") + log( + "warning", + f"Encountered {len(close_errors)} errors while closing cursors", + ) # Clear the cursor set explicitly to release any internal references self._cursors.clear() @@ -607,46 +664,50 @@ def close(self) -> None: if not self.autocommit: # If autocommit is disabled, rollback any uncommitted changes # This is important to ensure no partial transactions remain - # For autocommit True, this is not necessary as each statement is committed immediately - log('info', "Rolling back uncommitted changes before closing connection.") + # For autocommit True, this is not necessary as each + # statement is committed immediately + log( + "info", + "Rolling back uncommitted changes before closing connection.", + ) self._conn.rollback() # TODO: Check potential race conditions in case of multithreaded scenarios # Close the connection self._conn.close() self._conn = None except Exception as e: - log('error', f"Error closing database connection: {e}") + log("error", f"Error closing database connection: {e}") # Re-raise the connection close error as it's more critical raise finally: # Always mark as closed, even if there were errors self._closed = True - - log('info', "Connection closed successfully.") - def __enter__(self) -> 'Connection': + log("info", "Connection closed successfully.") + + def __enter__(self) -> "Connection": """ Enter the context manager. - + This method enables the Connection to be used with the 'with' statement. When entering the context, it simply returns the connection object itself. - + Returns: Connection: The connection object itself. - + Example: with connect(connection_string) as conn: cursor = conn.cursor() cursor.execute("INSERT INTO table VALUES (?)", [value]) # Transaction will be committed automatically when exiting """ - log('info', "Entering connection context manager.") + log("info", "Entering connection context manager.") return self def __exit__(self, *args) -> None: """ Exit the context manager. - + Closes the connection when exiting the context, ensuring proper resource cleanup. This follows the modern standard used by most database libraries. """ @@ -655,13 +716,14 @@ def __exit__(self, *args) -> None: def __del__(self): """ - Destructor to ensure the connection is closed when the connection object is no longer needed. + Destructor to ensure the connection is closed when the + connection object is no longer needed. This is a safety net to ensure resources are cleaned up even if close() was not called explicitly. """ if "_closed" not in self.__dict__ or not self._closed: try: self.close() - except Exception as e: + except (InterfaceError, DatabaseError) as e: # Dont raise exceptions from __del__ to avoid issues during garbage collection - log('error', f"Error during connection cleanup: {e}") \ No newline at end of file + log("error", f"Error during connection cleanup: {e}") diff --git a/mssql_python/constants.py b/mssql_python/constants.py index 20c8f663..c99ad688 100644 --- a/mssql_python/constants.py +++ b/mssql_python/constants.py @@ -11,6 +11,7 @@ class ConstantsDDBC(Enum): """ Constants used in the DDBC module. """ + SQL_HANDLE_ENV = 1 SQL_HANDLE_DBC = 2 SQL_HANDLE_STMT = 3 @@ -124,8 +125,10 @@ class ConstantsDDBC(Enum): SQL_FETCH_RELATIVE = 6 SQL_FETCH_BOOKMARK = 8 + class AuthType(Enum): """Constants for authentication types""" + INTERACTIVE = "activedirectoryinteractive" DEVICE_CODE = "activedirectorydevicecode" - DEFAULT = "activedirectorydefault" \ No newline at end of file + DEFAULT = "activedirectorydefault" diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index e2c811c9..e8ab4594 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -1,13 +1,16 @@ """ Copyright (c) Microsoft Corporation. Licensed under the MIT license. -This module contains the Cursor class, which represents a database cursor. +This module contains the Cursor class, which represents +a database cursor. + Resource Management: - Cursors are tracked by their parent connection. - Closing the connection will automatically close all open cursors. - Do not use a cursor after it is closed, or after its parent connection is closed. - Use close() to release resources held by the cursor as soon as it is no longer needed. """ + import decimal import uuid import datetime @@ -19,7 +22,10 @@ from .row import Row # Constants for string handling -MAX_INLINE_CHAR = 4000 # NVARCHAR/VARCHAR inline limit; this triggers NVARCHAR(MAX)/VARCHAR(MAX) + DAE +# NVARCHAR/VARCHAR inline limit; +# this triggers NVARCHAR(MAX)/VARCHAR(MAX) + DAE +MAX_INLINE_CHAR = 4000 + class Cursor: """ @@ -34,7 +40,7 @@ class Cursor: Methods: __init__(connection_str) -> None. - callproc(procname, parameters=None) -> + callproc(procname, parameters=None) -> Modified copy of the input sequence with output parameters. close() -> None. execute(operation, parameters=None) -> Cursor. @@ -76,12 +82,18 @@ def __init__(self, connection) -> None: # Is a list instead of a bool coz bools in Python are immutable. # Hence, we can't pass around bools by reference & modify them. # Therefore, it must be a list with exactly one bool element. - + # rownumber attribute - self._rownumber = -1 # DB-API extension: last returned row index, -1 before first - self._next_row_index = 0 # internal: index of the next row the driver will return (0-based) + self._rownumber = ( + -1 + ) # DB-API extension: last returned row index, -1 before first + self._next_row_index = ( + 0 # internal: index of the next row the driver will return (0-based) + ) self._has_result_set = False # Track if we have an active result set - self._skip_increment_for_next_fetch = False # Track if we need to skip incrementing the row index + self._skip_increment_for_next_fetch = ( + False # Track if we need to skip incrementing the row index + ) self.messages = [] # Store diagnostic messages @@ -163,7 +175,7 @@ def _parse_time(self, param): except ValueError: continue return None - + def _get_numeric_data(self, param): """ Get the data for a numeric parameter. @@ -172,7 +184,7 @@ def _get_numeric_data(self, param): param: The numeric parameter. Returns: - numeric_data: A NumericData struct containing + numeric_data: A NumericData struct containing the numeric data. """ decimal_as_tuple = param.as_tuple() @@ -220,7 +232,7 @@ def _get_numeric_data(self, param): def _map_sql_type(self, param, parameters_list, i): """ - Map a Python data type to the corresponding SQL type, + Map a Python data type to the corresponding SQL type, C type, Column size, and Decimal digits. Takes: - param: The parameter to map. @@ -239,7 +251,13 @@ def _map_sql_type(self, param, parameters_list, i): ) if isinstance(param, bool): - return ddbc_sql_const.SQL_BIT.value, ddbc_sql_const.SQL_C_BIT.value, 1, 0, False + return ( + ddbc_sql_const.SQL_BIT.value, + ddbc_sql_const.SQL_C_BIT.value, + 1, + 0, + False, + ) if isinstance(param, int): if 0 <= param <= 255: @@ -378,7 +396,7 @@ def _map_sql_type(self, param, parameters_list, i): 0, False, ) - + if isinstance(param, bytes): if len(param) > 8000: # Assuming VARBINARY(MAX) for long byte arrays return ( @@ -441,7 +459,9 @@ def _map_sql_type(self, param, parameters_list, i): ) # For safety: unknown/unhandled Python types should not silently go to SQL - raise TypeError("Unsupported parameter type: The driver cannot safely convert it to a SQL type.") + raise TypeError( + "Unsupported parameter type: The driver cannot safely convert it to a SQL type." + ) def _initialize_cursor(self) -> None: """ @@ -462,10 +482,10 @@ def _reset_cursor(self) -> None: if self.hstmt: self.hstmt.free() self.hstmt = None - log('debug', "SQLFreeHandle succeeded") - + log("debug", "SQLFreeHandle succeeded") + self._clear_rownumber() - + # Reinitialize the statement handle self._initialize_cursor() @@ -475,21 +495,21 @@ def close(self) -> None: The cursor will be unusable from this point forward; an InterfaceError will be raised if any operation is attempted with the cursor. - + Note: Unlike the current behavior, this method can be called multiple times safely. Subsequent calls to close() on an already closed cursor will have no effect. """ if self.closed: - return + return # Clear messages per DBAPI self.messages = [] - + if self.hstmt: self.hstmt.free() self.hstmt = None - log('debug', "SQLFreeHandle succeeded") + log("debug", "SQLFreeHandle succeeded") self._clear_rownumber() self.closed = True @@ -503,7 +523,7 @@ def _check_closed(self): if self.closed: raise InterfaceError( driver_error="Operation cannot be performed: the cursor is closed.", - ddbc_error="Operation cannot be performed: the cursor is closed." + ddbc_error="Operation cannot be performed: the cursor is closed.", ) def _create_parameter_types_list(self, parameter, param_info, parameters_list, i): @@ -587,52 +607,52 @@ def _map_data_type(self, sql_type): # Add more mappings as needed } return sql_to_python_type.get(sql_type, str) - + @property def rownumber(self): """ DB-API extension: Current 0-based index of the cursor in the result set. - + Returns: int or None: The current 0-based index of the cursor in the result set, or None if no row has been fetched yet or the index cannot be determined. - + Note: - Returns -1 before the first successful fetch - Returns 0 after fetching the first row - Returns -1 for empty result sets (since no rows can be fetched) - + Warning: This is a DB-API extension and may not be portable across different database modules. """ # Use mssql_python logging system instead of standard warnings - log('warning', "DB-API extension cursor.rownumber used") + log("warning", "DB-API extension cursor.rownumber used") # Return None if cursor is closed or no result set is available if self.closed or not self._has_result_set: return -1 - + return self._rownumber # Will be None until first fetch, then 0, 1, 2, etc. @property def connection(self): """ DB-API 2.0 attribute: Connection object that created this cursor. - + This is a read-only reference to the Connection object that was used to create this cursor. This attribute is useful for polymorphic code that needs access to connection-level functionality. - + Returns: Connection: The connection object that created this cursor. - + Note: This attribute is read-only as specified by DB-API 2.0. Attempting to assign to this attribute will raise an AttributeError. """ return self._connection - + def _reset_rownumber(self): """Reset the rownumber tracking when starting a new result set.""" self._rownumber = -1 @@ -650,13 +670,16 @@ def _increment_rownumber(self): # rownumber is last returned row index self._rownumber = self._next_row_index - 1 else: - raise InterfaceError("Cannot increment rownumber: no active result set.", "No active result set.") - + raise InterfaceError( + "Cannot increment rownumber: no active result set.", + "No active result set.", + ) + # Will be used when we add support for scrollable cursors def _decrement_rownumber(self): """ Decrement the rownumber by 1. - + This could be used for error recovery or cursor positioning operations. """ if self._has_result_set and self._rownumber >= 0: @@ -665,12 +688,15 @@ def _decrement_rownumber(self): else: self._rownumber = -1 else: - raise InterfaceError("Cannot decrement rownumber: no active result set.", "No active result set.") + raise InterfaceError( + "Cannot decrement rownumber: no active result set.", + "No active result set.", + ) def _clear_rownumber(self): """ Clear the rownumber tracking. - + This should be called when the result set is cleared or when the cursor is reset. """ self._rownumber = -1 @@ -680,22 +706,22 @@ def _clear_rownumber(self): def __iter__(self): """ Return the cursor itself as an iterator. - + This allows direct iteration over the cursor after execute(): - + for row in cursor.execute("SELECT * FROM table"): print(row) """ self._check_closed() return self - + def __next__(self): """ Fetch the next row when iterating over the cursor. - + Returns: The next Row object. - + Raises: StopIteration: When no more rows are available. """ @@ -704,16 +730,16 @@ def __next__(self): if row is None: raise StopIteration return row - + def next(self): """ Fetch the next row from the cursor. - + This is an alias for __next__() to maintain compatibility with older code. - + Returns: The next Row object. - + Raises: StopIteration: When no more rows are available. """ @@ -724,8 +750,8 @@ def execute( operation: str, *parameters, use_prepare: bool = True, - reset_cursor: bool = True - ) -> 'Cursor': + reset_cursor: bool = True, + ) -> "Cursor": """ Prepare and execute a database operation (query or command). @@ -763,23 +789,24 @@ def execute( # in low-memory conditions # (Ex: huge number of parallel queries with huge query string sizes) if operation != self.last_executed_stmt: -# Executing a new statement. Reset is_stmt_prepared to false + # Executing a new statement. Reset is_stmt_prepared to false self.is_stmt_prepared = [False] - log('debug', "Executing query: %s", operation) + log("debug", "Executing query: %s", operation) for i, param in enumerate(parameters): - log('debug', + log( + "debug", """Parameter number: %s, Parameter: %s, Param Python Type: %s, ParamInfo: %s, %s, %s, %s, %s""", i + 1, param, str(type(param)), - parameters_type[i].paramSQLType, - parameters_type[i].paramCType, - parameters_type[i].columnSize, - parameters_type[i].decimalDigits, - parameters_type[i].inputOutputType, - ) + parameters_type[i].paramSQLType, + parameters_type[i].paramCType, + parameters_type[i].columnSize, + parameters_type[i].decimalDigits, + parameters_type[i].inputOutputType, + ) ret = ddbc_bindings.DDBCSQLExecute( self.hstmt, @@ -791,19 +818,18 @@ def execute( ) # Check return code try: - - # Check for errors but don't raise exceptions for info/warning messages + + # Check for errors but don't raise exceptions for info/warning messages check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt, ret) except Exception as e: - log('warning', "Execute failed, resetting cursor: %s", e) + log("warning", "Execute failed, resetting cursor: %s", e) self._reset_cursor() raise - # Capture any diagnostic messages (SQL_SUCCESS_WITH_INFO, etc.) if self.hstmt: self.messages.extend(ddbc_bindings.DDBCSQLGetAllDiagRecords(self.hstmt)) - + self.last_executed_stmt = operation # Update rowcount after execution @@ -812,7 +838,7 @@ def execute( # Initialize description after execution self._initialize_description() - + # Reset rownumber for new result set (only for SELECT statements) if self.description: # If we have column descriptions, it's likely a SELECT self.rowcount = -1 @@ -830,7 +856,8 @@ def _select_best_sample_value(column): Selects the most representative non-null value from a column for type inference. This is used during executemany() to infer SQL/C types based on actual data, - preferring a non-null value that is not the first row to avoid bias from placeholder defaults. + preferring a non-null value that is not the + first row to avoid bias from placeholder defaults. Args: column: List of values in the column. @@ -840,7 +867,7 @@ def _select_best_sample_value(column): return None if all(isinstance(v, int) for v in non_nulls): # Pick the value with the widest range (min/max) - return max(non_nulls, key=lambda v: abs(v)) + return max(non_nulls, key=abs) if all(isinstance(v, float) for v in non_nulls): return 0.0 if all(isinstance(v, decimal.Decimal) for v in non_nulls): @@ -885,10 +912,10 @@ def executemany(self, operation: str, seq_of_parameters: list) -> None: """ self._check_closed() self._reset_cursor() - + # Clear any previous messages self.messages = [] - + if not seq_of_parameters: self.rowcount = 0 return @@ -902,12 +929,20 @@ def executemany(self, operation: str, seq_of_parameters: list) -> None: sample_value = self._select_best_sample_value(column) dummy_row = list(seq_of_parameters[0]) parameters_type.append( - self._create_parameter_types_list(sample_value, param_info, dummy_row, col_index) + self._create_parameter_types_list( + sample_value, param_info, dummy_row, col_index + ) ) columnwise_params = self._transpose_rowwise_to_columnwise(seq_of_parameters) - log('info', "Executing batch query with %d parameter sets:\n%s", - len(seq_of_parameters), "\n".join(f" {i+1}: {tuple(p) if isinstance(p, (list, tuple)) else p}" for i, p in enumerate(seq_of_parameters)) + log( + "info", + "Executing batch query with %d parameter sets:\n%s", + len(seq_of_parameters), + "\n".join( + f" {i+1}: {tuple(p) if isinstance(p, (list, tuple)) else p}" + for i, p in enumerate(seq_of_parameters) + ), ) # Execute batched statement @@ -916,18 +951,18 @@ def executemany(self, operation: str, seq_of_parameters: list) -> None: operation, columnwise_params, parameters_type, - len(seq_of_parameters) + len(seq_of_parameters), ) check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt, ret) # Capture any diagnostic messages after execution if self.hstmt: self.messages.extend(ddbc_bindings.DDBCSQLGetAllDiagRecords(self.hstmt)) - + self.rowcount = ddbc_bindings.DDBCSQLRowCount(self.hstmt) self.last_executed_stmt = operation self._initialize_description() - + if self.description: self.rowcount = -1 self._reset_rownumber() @@ -938,7 +973,7 @@ def executemany(self, operation: str, seq_of_parameters: list) -> None: def fetchone(self) -> Union[None, Row]: """ Fetch the next row of a query result set. - + Returns: Single Row object or None if no more data is available. """ @@ -948,23 +983,27 @@ def fetchone(self) -> Union[None, Row]: row_data = [] try: ret = ddbc_bindings.DDBCSQLFetchOne(self.hstmt, row_data) - + if self.hstmt: self.messages.extend(ddbc_bindings.DDBCSQLGetAllDiagRecords(self.hstmt)) - + if ret == ddbc_sql_const.SQL_NO_DATA.value: return None - + # Update internal position after successful fetch if self._skip_increment_for_next_fetch: self._skip_increment_for_next_fetch = False self._next_row_index += 1 else: self._increment_rownumber() - + # Create and return a Row object, passing column name map if available - column_map = getattr(self, '_column_name_map', None) - return Row(row_data, self.description, column_map) + column_map = getattr(self, "_column_name_map", None) + return Row( + row_data, + self.description, + column_map + ) except Exception as e: # On error, don't increment rownumber - rethrow the error raise e @@ -972,10 +1011,10 @@ def fetchone(self) -> Union[None, Row]: def fetchmany(self, size: int = None) -> List[Row]: """ Fetch the next set of rows of a query result. - + Args: size: Number of rows to fetch at a time. - + Returns: List of Row objects. """ @@ -988,25 +1027,27 @@ def fetchmany(self, size: int = None) -> List[Row]: if size <= 0: return [] - + # Fetch raw data rows_data = [] try: - ret = ddbc_bindings.DDBCSQLFetchMany(self.hstmt, rows_data, size) + ddbc_bindings.DDBCSQLFetchMany(self.hstmt, rows_data, size) if self.hstmt: self.messages.extend(ddbc_bindings.DDBCSQLGetAllDiagRecords(self.hstmt)) - - + # Update rownumber for the number of rows actually fetched if rows_data and self._has_result_set: # advance counters by number of rows actually returned self._next_row_index += len(rows_data) self._rownumber = self._next_row_index - 1 - + # Convert raw data to Row objects - column_map = getattr(self, '_column_name_map', None) - return [Row(row_data, self.description, column_map) for row_data in rows_data] + column_map = getattr(self, "_column_name_map", None) + return [ + Row(row_data, self.description, column_map) + for row_data in rows_data + ] except Exception as e: # On error, don't increment rownumber - rethrow the error raise e @@ -1014,7 +1055,7 @@ def fetchmany(self, size: int = None) -> List[Row]: def fetchall(self) -> List[Row]: """ Fetch all (remaining) rows of a query result. - + Returns: List of Row objects. """ @@ -1025,20 +1066,22 @@ def fetchall(self) -> List[Row]: # Fetch raw data rows_data = [] try: - ret = ddbc_bindings.DDBCSQLFetchAll(self.hstmt, rows_data) + ddbc_bindings.DDBCSQLFetchAll(self.hstmt, rows_data) if self.hstmt: self.messages.extend(ddbc_bindings.DDBCSQLGetAllDiagRecords(self.hstmt)) - - + # Update rownumber for the number of rows actually fetched if rows_data and self._has_result_set: self._next_row_index += len(rows_data) self._rownumber = self._next_row_index - 1 - + # Convert raw data to Row objects - column_map = getattr(self, '_column_name_map', None) - return [Row(row_data, self.description, column_map) for row_data in rows_data] + column_map = getattr(self, "_column_name_map", None) + return [ + Row(row_data, self.description, column_map) + for row_data in rows_data + ] except Exception as e: # On error, don't increment rownumber - rethrow the error raise e @@ -1057,11 +1100,11 @@ def nextset(self) -> Union[bool, None]: # Clear messages per DBAPI self.messages = [] - + # Skip to the next result set ret = ddbc_bindings.DDBCSQLMoreResults(self.hstmt) check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt, ret) - + if ret == ddbc_sql_const.SQL_NO_DATA.value: self._clear_rownumber() return False @@ -1073,13 +1116,13 @@ def nextset(self) -> Union[bool, None]: def __enter__(self): """ Enter the runtime context for the cursor. - + Returns: The cursor instance itself. """ self._check_closed() return self - + def __exit__(self, *args): """Closes the cursor when exiting the context, ensuring proper resource cleanup.""" if not self.closed: @@ -1089,94 +1132,94 @@ def __exit__(self, *args): def fetchval(self): """ Fetch the first column of the first row if there are results. - + This is a convenience method for queries that return a single value, such as SELECT COUNT(*) FROM table, SELECT MAX(id) FROM table, etc. - + Returns: The value of the first column of the first row, or None if no rows are available or the first column value is NULL. - + Raises: Exception: If the cursor is closed. - + Example: >>> count = cursor.execute('SELECT COUNT(*) FROM users').fetchval() >>> max_id = cursor.execute('SELECT MAX(id) FROM products').fetchval() >>> name = cursor.execute('SELECT name FROM users WHERE id = ?', user_id).fetchval() - + Note: This is a convenience extension beyond the DB-API 2.0 specification. After calling fetchval(), the cursor position advances by one row, just like fetchone(). """ self._check_closed() # Check if the cursor is closed - + # Check if this is a result-producing statement if not self.description: # Non-result-set statement (INSERT, UPDATE, DELETE, etc.) return None - + # Fetch the first row row = self.fetchone() - + return None if row is None else row[0] def commit(self): """ Commit all SQL statements executed on the connection that created this cursor. - + This is a convenience method that calls commit() on the underlying connection. It affects all cursors created by the same connection since the last commit/rollback. - + The benefit is that many uses can now just use the cursor and not have to track the connection object. - + Raises: Exception: If the cursor is closed or if the commit operation fails. - + Example: >>> cursor.execute("INSERT INTO users (name) VALUES (?)", "John") >>> cursor.commit() # Commits the INSERT - + Note: This is equivalent to calling connection.commit() but provides convenience for code that only has access to the cursor object. """ self._check_closed() # Check if the cursor is closed - + # Clear messages per DBAPI self.messages = [] - + # Delegate to the connection's commit method self._connection.commit() def rollback(self): """ Roll back all SQL statements executed on the connection that created this cursor. - + This is a convenience method that calls rollback() on the underlying connection. It affects all cursors created by the same connection since the last commit/rollback. - + The benefit is that many uses can now just use the cursor and not have to track the connection object. - + Raises: Exception: If the cursor is closed or if the rollback operation fails. - + Example: >>> cursor.execute("INSERT INTO users (name) VALUES (?)", "John") >>> cursor.rollback() # Rolls back the INSERT - + Note: This is equivalent to calling connection.rollback() but provides convenience for code that only has access to the cursor object. """ self._check_closed() # Check if the cursor is closed - + # Clear messages per DBAPI self.messages = [] - + # Delegate to the connection's rollback method self._connection.rollback() @@ -1191,160 +1234,174 @@ def __del__(self): self.close() except Exception as e: # Don't raise an exception in __del__, just log it - log('error', "Error during cursor cleanup in __del__: %s", e) + log("error", "Error during cursor cleanup in __del__: %s", e) - def scroll(self, value: int, mode: str = 'relative') -> None: + def scroll(self, value: int, mode: str = "relative") -> None: """ Scroll using SQLFetchScroll only, matching test semantics: - - relative(N>0): consume N rows; rownumber = previous + N; next fetch returns the following row. + - relative(N>0): consume N rows; rownumber = previous + N; + next fetch returns the following row. - absolute(-1): before first (rownumber = -1), no data consumed. - - absolute(0): position so next fetch returns first row; rownumber stays 0 even after that fetch. - - absolute(k>0): next fetch returns row index k (0-based); rownumber == k after scroll. + - absolute(0): position so next fetch returns first row; + rownumber stays 0 even after that fetch. + - absolute(k>0): next fetch returns row index k (0-based); + rownumber == k after scroll. """ self._check_closed() - + # Clear messages per DBAPI self.messages = [] - - if mode not in ('relative', 'absolute'): - raise ProgrammingError("Invalid scroll mode", - f"mode must be 'relative' or 'absolute', got '{mode}'") + + if mode not in ("relative", "absolute"): + raise ProgrammingError( + "Invalid scroll mode", + f"mode must be 'relative' or 'absolute', got '{mode}'", + ) if not self._has_result_set: - raise ProgrammingError("No active result set", - "Cannot scroll: no result set available. Execute a query first.") + raise ProgrammingError( + "No active result set", + "Cannot scroll: no result set available. Execute a query first.", + ) if not isinstance(value, int): - raise ProgrammingError("Invalid scroll value type", - f"scroll value must be an integer, got {type(value).__name__}") - + raise ProgrammingError( + "Invalid scroll value type", + f"scroll value must be an integer, got {type(value).__name__}", + ) + # Relative backward not supported - if mode == 'relative' and value < 0: - raise NotSupportedError("Backward scrolling not supported", - f"Cannot move backward by {value} rows on a forward-only cursor") - + if mode == "relative" and value < 0: + raise NotSupportedError( + "Backward scrolling not supported", + f"Cannot move backward by {value} rows on a forward-only cursor", + ) + row_data: list = [] - + # Absolute special cases - if mode == 'absolute': + if mode == "absolute": if value == -1: # Before first - ddbc_bindings.DDBCSQLFetchScroll(self.hstmt, - ddbc_sql_const.SQL_FETCH_ABSOLUTE.value, - 0, row_data) + ddbc_bindings.DDBCSQLFetchScroll( + self.hstmt, ddbc_sql_const.SQL_FETCH_ABSOLUTE.value, 0, row_data + ) self._rownumber = -1 self._next_row_index = 0 return if value == 0: # Before first, but tests want rownumber==0 pre and post the next fetch - ddbc_bindings.DDBCSQLFetchScroll(self.hstmt, - ddbc_sql_const.SQL_FETCH_ABSOLUTE.value, - 0, row_data) + ddbc_bindings.DDBCSQLFetchScroll( + self.hstmt, ddbc_sql_const.SQL_FETCH_ABSOLUTE.value, 0, row_data + ) self._rownumber = 0 self._next_row_index = 0 self._skip_increment_for_next_fetch = True return - + try: - if mode == 'relative': + if mode == "relative": if value == 0: return - ret = ddbc_bindings.DDBCSQLFetchScroll(self.hstmt, - ddbc_sql_const.SQL_FETCH_RELATIVE.value, - value, row_data) + ret = ddbc_bindings.DDBCSQLFetchScroll( + self.hstmt, ddbc_sql_const.SQL_FETCH_RELATIVE.value, value, row_data + ) if ret == ddbc_sql_const.SQL_NO_DATA.value: - raise IndexError("Cannot scroll to specified position: end of result set reached") + raise IndexError( + "Cannot scroll to specified position: end of result set reached" + ) # Consume N rows; last-returned index advances by N self._rownumber = self._rownumber + value self._next_row_index = self._rownumber + 1 return - + # absolute(k>0): map Python k (0-based next row) to ODBC ABSOLUTE k (1-based), # intentionally passing k so ODBC fetches row #k (1-based), i.e., 0-based (k-1), # leaving the NEXT fetch to return 0-based index k. - ret = ddbc_bindings.DDBCSQLFetchScroll(self.hstmt, - ddbc_sql_const.SQL_FETCH_ABSOLUTE.value, - value, row_data) + ret = ddbc_bindings.DDBCSQLFetchScroll( + self.hstmt, ddbc_sql_const.SQL_FETCH_ABSOLUTE.value, value, row_data + ) if ret == ddbc_sql_const.SQL_NO_DATA.value: - raise IndexError(f"Cannot scroll to position {value}: end of result set reached") - + raise IndexError( + f"Cannot scroll to position {value}: end of result set reached" + ) + # Tests expect rownumber == value after absolute(value) # Next fetch should return row index 'value' self._rownumber = value self._next_row_index = value - + except Exception as e: if isinstance(e, (IndexError, NotSupportedError)): raise raise IndexError(f"Scroll operation failed: {e}") from e - + def skip(self, count: int) -> None: """ Skip the next count records in the query result set. - + Args: count: Number of records to skip. - + Raises: IndexError: If attempting to skip past the end of the result set. ProgrammingError: If count is not an integer. NotSupportedError: If attempting to skip backwards. """ - from mssql_python.exceptions import ProgrammingError, NotSupportedError - self._check_closed() - + # Clear messages self.messages = [] - + # Simply delegate to the scroll method with 'relative' mode - self.scroll(count, 'relative') + self.scroll(count, "relative") - def _execute_tables(self, stmt_handle, catalog_name=None, schema_name=None, table_name=None, - table_type=None, search_escape=None): + def _execute_tables( + self, + stmt_handle, + catalog_name=None, + schema_name=None, + table_name=None, + table_type=None + ): """ Execute SQLTables ODBC function to retrieve table metadata. - + Args: stmt_handle: ODBC statement handle catalog_name: The catalog name pattern schema_name: The schema name pattern table_name: The table name pattern table_type: The table type filter - search_escape: The escape character for pattern matching """ # Convert None values to empty strings for ODBC catalog = "" if catalog_name is None else catalog_name schema = "" if schema_name is None else schema_name table = "" if table_name is None else table_name types = "" if table_type is None else table_type - + # Call the ODBC SQLTables function retcode = ddbc_bindings.DDBCSQLTables( - stmt_handle, - catalog, - schema, - table, - types + stmt_handle, catalog, schema, table, types ) - + # Check return code and handle errors check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, stmt_handle, retcode) - + # Capture any diagnostic messages if stmt_handle: self.messages.extend(ddbc_bindings.DDBCSQLGetAllDiagRecords(stmt_handle)) - def tables(self, table=None, catalog=None, schema=None, tableType=None): + def tables(self, table=None, catalog=None, schema=None, table_type=None): """ Returns information about tables in the database that match the given criteria using the SQLTables ODBC function. - + Args: table (str, optional): The table name pattern. Default is None (all tables). catalog (str, optional): The catalog name. Default is None. schema (str, optional): The schema name pattern. Default is None. - tableType (str or list, optional): The table type filter. Default is None. + table_type (str or list, optional): The table type filter. Default is None. Example: "TABLE" or ["TABLE", "VIEW"] - + Returns: list: A list of Row objects containing table information with these columns: - table_cat: Catalog name @@ -1352,50 +1409,50 @@ def tables(self, table=None, catalog=None, schema=None, tableType=None): - table_name: Table name - table_type: Table type (e.g., "TABLE", "VIEW") - remarks: Comments about the table - + Notes: This method only processes the standard five columns as defined in the ODBC specification. Any additional columns that might be returned by specific ODBC drivers are not included in the result set. - + Example: # Get all tables in the database tables = cursor.tables() - + # Get all tables in schema 'dbo' tables = cursor.tables(schema='dbo') - + # Get table named 'Customers' tables = cursor.tables(table='Customers') - + # Get all views - tables = cursor.tables(tableType='VIEW') + tables = cursor.tables(table_type='VIEW') """ self._check_closed() - + # Clear messages self.messages = [] - + # Always reset the cursor first to ensure clean state self._reset_cursor() - + # Format table_type parameter - SQLTables expects comma-separated string table_type_str = None - if tableType is not None: - if isinstance(tableType, (list, tuple)): - table_type_str = ",".join(tableType) + if table_type is not None: + if isinstance(table_type, (list, tuple)): + table_type_str = ",".join(table_type) else: - table_type_str = str(tableType) - + table_type_str = str(table_type) + # Call SQLTables via the helper method self._execute_tables( self.hstmt, catalog_name=catalog, schema_name=schema, table_name=table, - table_type=table_type_str + table_type=table_type_str, ) - + # Initialize description from column metadata column_metadata = [] try: @@ -1409,25 +1466,29 @@ def tables(self, table=None, catalog=None, schema=None, tableType=None): ("table_schem", column_types[1], None, 128, 128, 0, True), ("table_name", column_types[2], None, 128, 128, 0, False), ("table_type", column_types[3], None, 128, 128, 0, False), - ("remarks", column_types[4], None, 254, 254, 0, True) + ("remarks", column_types[4], None, 254, 254, 0, True), ] - + # Define column names in ODBC standard order column_names = [ - "table_cat", "table_schem", "table_name", "table_type", "remarks" + "table_cat", + "table_schem", + "table_name", + "table_type", + "remarks", ] - + # Fetch all rows rows_data = [] ddbc_bindings.DDBCSQLFetchAll(self.hstmt, rows_data) - + # Create a column map for attribute access column_map = {name: i for i, name in enumerate(column_names)} - + # Create Row objects with the column map result_rows = [] for row_data in rows_data: row = Row(row_data, self.description, column_map) result_rows.append(row) - - return result_rows \ No newline at end of file + + return result_rows diff --git a/mssql_python/db_connection.py b/mssql_python/db_connection.py index 5e98056e..15ee23a5 100644 --- a/mssql_python/db_connection.py +++ b/mssql_python/db_connection.py @@ -3,9 +3,16 @@ Licensed under the MIT license. This module provides a way to create a new connection object to interact with the database. """ + from mssql_python.connection import Connection -def connect(connection_str: str = "", autocommit: bool = False, attrs_before: dict = None, **kwargs) -> Connection: + +def connect( + connection_str: str = "", + autocommit: bool = False, + attrs_before: dict = None, + **kwargs +) -> Connection: """ Constructor for creating a connection to the database. @@ -33,5 +40,7 @@ def connect(connection_str: str = "", autocommit: bool = False, attrs_before: di be used to perform database operations such as executing queries, committing transactions, and closing the connection. """ - conn = Connection(connection_str, autocommit=autocommit, attrs_before=attrs_before, **kwargs) + conn = Connection( + connection_str, autocommit=autocommit, attrs_before=attrs_before, **kwargs + ) return conn diff --git a/mssql_python/ddbc_bindings.py b/mssql_python/ddbc_bindings.py index 1d4d32cb..a038f7e6 100644 --- a/mssql_python/ddbc_bindings.py +++ b/mssql_python/ddbc_bindings.py @@ -3,53 +3,67 @@ import sys import platform + def normalize_architecture(platform_name, architecture): """ Normalize architecture names for the given platform. - + Args: platform_name (str): Platform name ('windows', 'darwin', 'linux') architecture (str): Architecture string to normalize - + Returns: str: Normalized architecture name - + Raises: ImportError: If architecture is not supported for the given platform OSError: If platform is not supported """ arch_lower = architecture.lower() - + if platform_name == "windows": arch_map = { - "win64": "x64", "amd64": "x64", "x64": "x64", - "win32": "x86", "x86": "x86", - "arm64": "arm64" + "win64": "x64", + "amd64": "x64", + "x64": "x64", + "win32": "x86", + "x86": "x86", + "arm64": "arm64", } if arch_lower in arch_map: return arch_map[arch_lower] else: supported = list(set(arch_map.keys())) - raise ImportError(f"Unsupported architecture '{architecture}' for platform '{platform_name}'; expected one of {supported}") - + raise ImportError( + f"Unsupported architecture '{architecture}' for platform '{platform_name}'; expected one of {supported}" + ) + elif platform_name == "darwin": # For macOS, return runtime architecture return platform.machine().lower() - + elif platform_name == "linux": arch_map = { - "x64": "x86_64", "amd64": "x86_64", "x86_64": "x86_64", - "arm64": "arm64", "aarch64": "arm64" + "x64": "x86_64", + "amd64": "x86_64", + "x86_64": "x86_64", + "arm64": "arm64", + "aarch64": "arm64", } if arch_lower in arch_map: return arch_map[arch_lower] else: supported = list(set(arch_map.keys())) - raise ImportError(f"Unsupported architecture '{architecture}' for platform '{platform_name}'; expected one of {supported}") - + raise ImportError( + f"Unsupported architecture '{architecture}' for platform '{platform_name}'; expected one of {supported}" + ) + else: supported_platforms = ["windows", "darwin", "linux"] - raise OSError(f"Unsupported platform '{platform_name}'; expected one of {supported_platforms}") + raise OSError( + f"Unsupported platform '{platform_name}'; expected one of {supported_platforms}" + ) + # Get current Python version and architecture python_version = f"cp{sys.version_info.major}{sys.version_info.minor}" @@ -58,25 +72,27 @@ def normalize_architecture(platform_name, architecture): raw_architecture = platform.machine().lower() # Special handling for macOS universal2 binaries -if platform_name == 'darwin': +if platform_name == "darwin": architecture = "universal2" else: architecture = normalize_architecture(platform_name, raw_architecture) - + # Handle Windows-specific naming for binary files - if platform_name == 'windows' and architecture == 'x64': + if platform_name == "windows" and architecture == "x64": architecture = "amd64" # Validate supported platforms -if platform_name not in ['windows', 'darwin', 'linux']: - supported_platforms = ['windows', 'darwin', 'linux'] - raise ImportError(f"Unsupported platform '{platform_name}' for mssql-python; expected one of {supported_platforms}") +if platform_name not in ["windows", "darwin", "linux"]: + supported_platforms = ["windows", "darwin", "linux"] + raise ImportError( + f"Unsupported platform '{platform_name}' for mssql-python; expected one of {supported_platforms}" + ) # Determine extension based on platform -if platform_name == 'windows': - extension = '.pyd' +if platform_name == "windows": + extension = ".pyd" else: # macOS or Linux - extension = '.so' + extension = ".so" # Find the specifically matching module file module_dir = os.path.dirname(__file__) @@ -85,11 +101,19 @@ def normalize_architecture(platform_name, architecture): if not os.path.exists(module_path): # Fallback to searching for any matching module if the specific one isn't found - module_files = [f for f in os.listdir(module_dir) if f.startswith('ddbc_bindings.') and f.endswith(extension)] + module_files = [ + f + for f in os.listdir(module_dir) + if f.startswith("ddbc_bindings.") and f.endswith(extension) + ] if not module_files: - raise ImportError(f"No ddbc_bindings module found for {python_version}-{architecture} with extension {extension}") + raise ImportError( + f"No ddbc_bindings module found for {python_version}-{architecture} with extension {extension}" + ) module_path = os.path.join(module_dir, module_files[0]) - print(f"Warning: Using fallback module file {module_files[0]} instead of {expected_module}") + print( + f"Warning: Using fallback module file {module_files[0]} instead of {expected_module}" + ) # Use the original module name 'ddbc_bindings' that the C extension was compiled with name = "ddbc_bindings" @@ -100,5 +124,5 @@ def normalize_architecture(platform_name, architecture): # Copy all attributes from the loaded module to this module for attr in dir(module): - if not attr.startswith('__'): - globals()[attr] = getattr(module, attr) \ No newline at end of file + if not attr.startswith("__"): + globals()[attr] = getattr(module, attr) diff --git a/mssql_python/exceptions.py b/mssql_python/exceptions.py index 308a8569..7dad8997 100644 --- a/mssql_python/exceptions.py +++ b/mssql_python/exceptions.py @@ -4,6 +4,7 @@ This module contains custom exception classes for the mssql_python package. These classes are used to raise exceptions when an error occurs while executing a query. """ + from mssql_python.logging_config import get_logger logger = get_logger() @@ -136,68 +137,53 @@ def sqlstate_to_exception(sqlstate: str, ddbc_error: str) -> Exception: """ mapping = { "01000": Warning( - driver_error="General warning", - ddbc_error=ddbc_error + driver_error="General warning", ddbc_error=ddbc_error ), # General warning "01001": OperationalError( - driver_error="Cursor operation conflict", - ddbc_error=ddbc_error + driver_error="Cursor operation conflict", ddbc_error=ddbc_error ), # Cursor operation conflict "01002": OperationalError( - driver_error="Disconnect error", - ddbc_error=ddbc_error + driver_error="Disconnect error", ddbc_error=ddbc_error ), # Disconnect error "01003": DataError( - driver_error="NULL value eliminated in set function", - ddbc_error=ddbc_error + driver_error="NULL value eliminated in set function", ddbc_error=ddbc_error ), # NULL value eliminated in set function "01004": DataError( - driver_error="String data, right-truncated", - ddbc_error=ddbc_error + driver_error="String data, right-truncated", ddbc_error=ddbc_error ), # String data, right-truncated "01006": OperationalError( - driver_error="Privilege not revoked", - ddbc_error=ddbc_error + driver_error="Privilege not revoked", ddbc_error=ddbc_error ), # Privilege not revoked "01007": OperationalError( - driver_error="Privilege not granted", - ddbc_error=ddbc_error + driver_error="Privilege not granted", ddbc_error=ddbc_error ), # Privilege not granted "01S00": ProgrammingError( - driver_error="Invalid connection string attribute", - ddbc_error=ddbc_error + driver_error="Invalid connection string attribute", ddbc_error=ddbc_error ), # Invalid connection string attribute "01S01": DataError( - driver_error="Error in row", - ddbc_error=ddbc_error + driver_error="Error in row", ddbc_error=ddbc_error ), # Error in row "01S02": Warning( - driver_error="Option value changed", - ddbc_error=ddbc_error + driver_error="Option value changed", ddbc_error=ddbc_error ), # Option value changed "01S06": OperationalError( driver_error="Attempt to fetch before the result set returned the first rowset", ddbc_error=ddbc_error, ), # Attempt to fetch before the result set returned the first rowset "01S07": DataError( - driver_error="Fractional truncation", - ddbc_error=ddbc_error + driver_error="Fractional truncation", ddbc_error=ddbc_error ), # Fractional truncation "01S08": OperationalError( - driver_error="Error saving File DSN", - ddbc_error=ddbc_error + driver_error="Error saving File DSN", ddbc_error=ddbc_error ), # Error saving File DSN "01S09": ProgrammingError( - driver_error="Invalid keyword", - ddbc_error=ddbc_error + driver_error="Invalid keyword", ddbc_error=ddbc_error ), # Invalid keyword "07001": ProgrammingError( - driver_error="Wrong number of parameters", - ddbc_error=ddbc_error + driver_error="Wrong number of parameters", ddbc_error=ddbc_error ), # Wrong number of parameters "07002": ProgrammingError( - driver_error="COUNT field incorrect", - ddbc_error=ddbc_error + driver_error="COUNT field incorrect", ddbc_error=ddbc_error ), # COUNT field incorrect "07005": ProgrammingError( driver_error="Prepared statement not a cursor-specification", @@ -208,36 +194,28 @@ def sqlstate_to_exception(sqlstate: str, ddbc_error: str) -> Exception: ddbc_error=ddbc_error, ), # Restricted data type attribute violation "07009": ProgrammingError( - driver_error="Invalid descriptor index", - ddbc_error=ddbc_error + driver_error="Invalid descriptor index", ddbc_error=ddbc_error ), # Invalid descriptor index "07S01": ProgrammingError( - driver_error="Invalid use of default parameter", - ddbc_error=ddbc_error + driver_error="Invalid use of default parameter", ddbc_error=ddbc_error ), # Invalid use of default parameter "08001": OperationalError( - driver_error="Client unable to establish connection", - ddbc_error=ddbc_error + driver_error="Client unable to establish connection", ddbc_error=ddbc_error ), # Client unable to establish connection "08002": OperationalError( - driver_error="Connection name in use", - ddbc_error=ddbc_error + driver_error="Connection name in use", ddbc_error=ddbc_error ), # Connection name in use "08003": OperationalError( - driver_error="Connection not open", - ddbc_error=ddbc_error + driver_error="Connection not open", ddbc_error=ddbc_error ), # Connection not open "08004": OperationalError( - driver_error="Server rejected the connection", - ddbc_error=ddbc_error + driver_error="Server rejected the connection", ddbc_error=ddbc_error ), # Server rejected the connection "08007": OperationalError( - driver_error="Connection failure during transaction", - ddbc_error=ddbc_error + driver_error="Connection failure during transaction", ddbc_error=ddbc_error ), # Connection failure during transaction "08S01": OperationalError( - driver_error="Communication link failure", - ddbc_error=ddbc_error + driver_error="Communication link failure", ddbc_error=ddbc_error ), # Communication link failure "21S01": ProgrammingError( driver_error="Insert value list does not match column list", @@ -248,188 +226,145 @@ def sqlstate_to_exception(sqlstate: str, ddbc_error: str) -> Exception: ddbc_error=ddbc_error, ), # Degree of derived table does not match column list "22001": DataError( - driver_error="String data, right-truncated", - ddbc_error=ddbc_error + driver_error="String data, right-truncated", ddbc_error=ddbc_error ), # String data, right-truncated "22002": DataError( driver_error="Indicator variable required but not supplied", ddbc_error=ddbc_error, ), # Indicator variable required but not supplied "22003": DataError( - driver_error="Numeric value out of range", - ddbc_error=ddbc_error + driver_error="Numeric value out of range", ddbc_error=ddbc_error ), # Numeric value out of range "22007": DataError( - driver_error="Invalid datetime format", - ddbc_error=ddbc_error + driver_error="Invalid datetime format", ddbc_error=ddbc_error ), # Invalid datetime format "22008": DataError( - driver_error="Datetime field overflow", - ddbc_error=ddbc_error + driver_error="Datetime field overflow", ddbc_error=ddbc_error ), # Datetime field overflow "22012": DataError( - driver_error="Division by zero", - ddbc_error=ddbc_error + driver_error="Division by zero", ddbc_error=ddbc_error ), # Division by zero "22015": DataError( - driver_error="Interval field overflow", - ddbc_error=ddbc_error + driver_error="Interval field overflow", ddbc_error=ddbc_error ), # Interval field overflow "22018": DataError( driver_error="Invalid character value for cast specification", ddbc_error=ddbc_error, ), # Invalid character value for cast specification "22019": ProgrammingError( - driver_error="Invalid escape character", - ddbc_error=ddbc_error + driver_error="Invalid escape character", ddbc_error=ddbc_error ), # Invalid escape character "22025": ProgrammingError( - driver_error="Invalid escape sequence", - ddbc_error=ddbc_error + driver_error="Invalid escape sequence", ddbc_error=ddbc_error ), # Invalid escape sequence "22026": DataError( - driver_error="String data, length mismatch", - ddbc_error=ddbc_error + driver_error="String data, length mismatch", ddbc_error=ddbc_error ), # String data, length mismatch "23000": IntegrityError( - driver_error="Integrity constraint violation", - ddbc_error=ddbc_error + driver_error="Integrity constraint violation", ddbc_error=ddbc_error ), # Integrity constraint violation "24000": ProgrammingError( - driver_error="Invalid cursor state", - ddbc_error=ddbc_error + driver_error="Invalid cursor state", ddbc_error=ddbc_error ), # Invalid cursor state "25000": OperationalError( - driver_error="Invalid transaction state", - ddbc_error=ddbc_error + driver_error="Invalid transaction state", ddbc_error=ddbc_error ), # Invalid transaction state "25S01": OperationalError( - driver_error="Transaction state", - ddbc_error=ddbc_error + driver_error="Transaction state", ddbc_error=ddbc_error ), # Transaction state "25S02": OperationalError( - driver_error="Transaction is still active", - ddbc_error=ddbc_error + driver_error="Transaction is still active", ddbc_error=ddbc_error ), # Transaction is still active "25S03": OperationalError( - driver_error="Transaction is rolled back", - ddbc_error=ddbc_error + driver_error="Transaction is rolled back", ddbc_error=ddbc_error ), # Transaction is rolled back "28000": OperationalError( - driver_error="Invalid authorization specification", - ddbc_error=ddbc_error + driver_error="Invalid authorization specification", ddbc_error=ddbc_error ), # Invalid authorization specification "34000": ProgrammingError( - driver_error="Invalid cursor name", - ddbc_error=ddbc_error + driver_error="Invalid cursor name", ddbc_error=ddbc_error ), # Invalid cursor name "3C000": ProgrammingError( - driver_error="Duplicate cursor name", - ddbc_error=ddbc_error + driver_error="Duplicate cursor name", ddbc_error=ddbc_error ), # Duplicate cursor name "3D000": ProgrammingError( - driver_error="Invalid catalog name", - ddbc_error=ddbc_error + driver_error="Invalid catalog name", ddbc_error=ddbc_error ), # Invalid catalog name "3F000": ProgrammingError( - driver_error="Invalid schema name", - ddbc_error=ddbc_error + driver_error="Invalid schema name", ddbc_error=ddbc_error ), # Invalid schema name "40001": OperationalError( - driver_error="Serialization failure", - ddbc_error=ddbc_error + driver_error="Serialization failure", ddbc_error=ddbc_error ), # Serialization failure "40002": IntegrityError( - driver_error="Integrity constraint violation", - ddbc_error=ddbc_error + driver_error="Integrity constraint violation", ddbc_error=ddbc_error ), # Integrity constraint violation "40003": OperationalError( - driver_error="Statement completion unknown", - ddbc_error=ddbc_error + driver_error="Statement completion unknown", ddbc_error=ddbc_error ), # Statement completion unknown "42000": ProgrammingError( - driver_error="Syntax error or access violation", - ddbc_error=ddbc_error + driver_error="Syntax error or access violation", ddbc_error=ddbc_error ), # Syntax error or access violation "42S01": ProgrammingError( - driver_error="Base table or view already exists", - ddbc_error=ddbc_error + driver_error="Base table or view already exists", ddbc_error=ddbc_error ), # Base table or view already exists "42S02": ProgrammingError( - driver_error="Base table or view not found", - ddbc_error=ddbc_error + driver_error="Base table or view not found", ddbc_error=ddbc_error ), # Base table or view not found "42S11": ProgrammingError( - driver_error="Index already exists", - ddbc_error=ddbc_error + driver_error="Index already exists", ddbc_error=ddbc_error ), # Index already exists "42S12": ProgrammingError( - driver_error="Index not found", - ddbc_error=ddbc_error + driver_error="Index not found", ddbc_error=ddbc_error ), # Index not found "42S21": ProgrammingError( - driver_error="Column already exists", - ddbc_error=ddbc_error + driver_error="Column already exists", ddbc_error=ddbc_error ), # Column already exists "42S22": ProgrammingError( - driver_error="Column not found", - ddbc_error=ddbc_error + driver_error="Column not found", ddbc_error=ddbc_error ), # Column not found "44000": IntegrityError( - driver_error="WITH CHECK OPTION violation", - ddbc_error=ddbc_error + driver_error="WITH CHECK OPTION violation", ddbc_error=ddbc_error ), # WITH CHECK OPTION violation "HY000": OperationalError( - driver_error="General error", - ddbc_error=ddbc_error + driver_error="General error", ddbc_error=ddbc_error ), # General error "HY001": OperationalError( - driver_error="Memory allocation error", - ddbc_error=ddbc_error + driver_error="Memory allocation error", ddbc_error=ddbc_error ), # Memory allocation error "HY003": ProgrammingError( - driver_error="Invalid application buffer type", - ddbc_error=ddbc_error + driver_error="Invalid application buffer type", ddbc_error=ddbc_error ), # Invalid application buffer type "HY004": ProgrammingError( - driver_error="Invalid SQL data type", - ddbc_error=ddbc_error + driver_error="Invalid SQL data type", ddbc_error=ddbc_error ), # Invalid SQL data type "HY007": ProgrammingError( - driver_error="Associated statement is not prepared", - ddbc_error=ddbc_error + driver_error="Associated statement is not prepared", ddbc_error=ddbc_error ), # Associated statement is not prepared "HY008": OperationalError( - driver_error="Operation canceled", - ddbc_error=ddbc_error + driver_error="Operation canceled", ddbc_error=ddbc_error ), # Operation canceled "HY009": ProgrammingError( - driver_error="Invalid use of null pointer", - ddbc_error=ddbc_error + driver_error="Invalid use of null pointer", ddbc_error=ddbc_error ), # Invalid use of null pointer "HY010": ProgrammingError( - driver_error="Function sequence error", - ddbc_error=ddbc_error + driver_error="Function sequence error", ddbc_error=ddbc_error ), # Function sequence error "HY011": ProgrammingError( - driver_error="Attribute cannot be set now", - ddbc_error=ddbc_error + driver_error="Attribute cannot be set now", ddbc_error=ddbc_error ), # Attribute cannot be set now "HY012": ProgrammingError( - driver_error="Invalid transaction operation code", - ddbc_error=ddbc_error + driver_error="Invalid transaction operation code", ddbc_error=ddbc_error ), # Invalid transaction operation code "HY013": OperationalError( - driver_error="Memory management error", - ddbc_error=ddbc_error + driver_error="Memory management error", ddbc_error=ddbc_error ), # Memory management error "HY014": OperationalError( driver_error="Limit on the number of handles exceeded", ddbc_error=ddbc_error, ), # Limit on the number of handles exceeded "HY015": ProgrammingError( - driver_error="No cursor name available", - ddbc_error=ddbc_error + driver_error="No cursor name available", ddbc_error=ddbc_error ), # No cursor name available "HY016": ProgrammingError( driver_error="Cannot modify an implementation row descriptor", @@ -440,120 +375,93 @@ def sqlstate_to_exception(sqlstate: str, ddbc_error: str) -> Exception: ddbc_error=ddbc_error, ), # Invalid use of an automatically allocated descriptor handle "HY018": OperationalError( - driver_error="Server declined cancel request", - ddbc_error=ddbc_error + driver_error="Server declined cancel request", ddbc_error=ddbc_error ), # Server declined cancel request "HY019": DataError( driver_error="Non-character and non-binary data sent in pieces", ddbc_error=ddbc_error, ), # Non-character and non-binary data sent in pieces "HY020": DataError( - driver_error="Attempt to concatenate a null value", - ddbc_error=ddbc_error + driver_error="Attempt to concatenate a null value", ddbc_error=ddbc_error ), # Attempt to concatenate a null value "HY021": ProgrammingError( - driver_error="Inconsistent descriptor information", - ddbc_error=ddbc_error + driver_error="Inconsistent descriptor information", ddbc_error=ddbc_error ), # Inconsistent descriptor information "HY024": ProgrammingError( - driver_error="Invalid attribute value", - ddbc_error=ddbc_error + driver_error="Invalid attribute value", ddbc_error=ddbc_error ), # Invalid attribute value "HY090": ProgrammingError( - driver_error="Invalid string or buffer length", - ddbc_error=ddbc_error + driver_error="Invalid string or buffer length", ddbc_error=ddbc_error ), # Invalid string or buffer length "HY091": ProgrammingError( - driver_error="Invalid descriptor field identifier", - ddbc_error=ddbc_error + driver_error="Invalid descriptor field identifier", ddbc_error=ddbc_error ), # Invalid descriptor field identifier "HY092": ProgrammingError( - driver_error="Invalid attribute/option identifier", - ddbc_error=ddbc_error + driver_error="Invalid attribute/option identifier", ddbc_error=ddbc_error ), # Invalid attribute/option identifier "HY095": ProgrammingError( - driver_error="Function type out of range", - ddbc_error=ddbc_error + driver_error="Function type out of range", ddbc_error=ddbc_error ), # Function type out of range "HY096": ProgrammingError( - driver_error="Invalid information type", - ddbc_error=ddbc_error + driver_error="Invalid information type", ddbc_error=ddbc_error ), # Invalid information type "HY097": ProgrammingError( - driver_error="Column type out of range", - ddbc_error=ddbc_error + driver_error="Column type out of range", ddbc_error=ddbc_error ), # Column type out of range "HY098": ProgrammingError( - driver_error="Scope type out of range", - ddbc_error=ddbc_error + driver_error="Scope type out of range", ddbc_error=ddbc_error ), # Scope type out of range "HY099": ProgrammingError( - driver_error="Nullable type out of range", - ddbc_error=ddbc_error + driver_error="Nullable type out of range", ddbc_error=ddbc_error ), # Nullable type out of range "HY100": ProgrammingError( - driver_error="Uniqueness option type out of range", - ddbc_error=ddbc_error + driver_error="Uniqueness option type out of range", ddbc_error=ddbc_error ), # Uniqueness option type out of range "HY101": ProgrammingError( - driver_error="Accuracy option type out of range", - ddbc_error=ddbc_error + driver_error="Accuracy option type out of range", ddbc_error=ddbc_error ), # Accuracy option type out of range "HY103": ProgrammingError( - driver_error="Invalid retrieval code", - ddbc_error=ddbc_error + driver_error="Invalid retrieval code", ddbc_error=ddbc_error ), # Invalid retrieval code "HY104": ProgrammingError( - driver_error="Invalid precision or scale value", - ddbc_error=ddbc_error + driver_error="Invalid precision or scale value", ddbc_error=ddbc_error ), # Invalid precision or scale value "HY105": ProgrammingError( - driver_error="Invalid parameter type", - ddbc_error=ddbc_error + driver_error="Invalid parameter type", ddbc_error=ddbc_error ), # Invalid parameter type "HY106": ProgrammingError( - driver_error="Fetch type out of range", - ddbc_error=ddbc_error + driver_error="Fetch type out of range", ddbc_error=ddbc_error ), # Fetch type out of range "HY107": ProgrammingError( - driver_error="Row value out of range", - ddbc_error=ddbc_error + driver_error="Row value out of range", ddbc_error=ddbc_error ), # Row value out of range "HY109": ProgrammingError( - driver_error="Invalid cursor position", - ddbc_error=ddbc_error + driver_error="Invalid cursor position", ddbc_error=ddbc_error ), # Invalid cursor position "HY110": ProgrammingError( - driver_error="Invalid driver completion", - ddbc_error=ddbc_error + driver_error="Invalid driver completion", ddbc_error=ddbc_error ), # Invalid driver completion "HY111": ProgrammingError( - driver_error="Invalid bookmark value", - ddbc_error=ddbc_error + driver_error="Invalid bookmark value", ddbc_error=ddbc_error ), # Invalid bookmark value "HYC00": NotSupportedError( - driver_error="Optional feature not implemented", - ddbc_error=ddbc_error + driver_error="Optional feature not implemented", ddbc_error=ddbc_error ), # Optional feature not implemented "HYT00": OperationalError( - driver_error="Timeout expired", - ddbc_error=ddbc_error + driver_error="Timeout expired", ddbc_error=ddbc_error ), # Timeout expired "HYT01": OperationalError( - driver_error="Connection timeout expired", - ddbc_error=ddbc_error + driver_error="Connection timeout expired", ddbc_error=ddbc_error ), # Connection timeout expired "IM001": NotSupportedError( - driver_error="Driver does not support this function", - ddbc_error=ddbc_error + driver_error="Driver does not support this function", ddbc_error=ddbc_error ), # Driver does not support this function "IM002": OperationalError( driver_error="Data source name not found and no default driver specified", ddbc_error=ddbc_error, ), # Data source name not found and no default driver specified "IM003": OperationalError( - driver_error="Specified driver could not be loaded", - ddbc_error=ddbc_error + driver_error="Specified driver could not be loaded", ddbc_error=ddbc_error ), # Specified driver could not be loaded "IM004": OperationalError( driver_error="Driver's SQLAllocHandle on SQL_HANDLE_ENV failed", @@ -564,44 +472,35 @@ def sqlstate_to_exception(sqlstate: str, ddbc_error: str) -> Exception: ddbc_error=ddbc_error, ), # Driver's SQLAllocHandle on SQL_HANDLE_DBC failed "IM006": OperationalError( - driver_error="Driver's SQLSetConnectAttr failed", - ddbc_error=ddbc_error + driver_error="Driver's SQLSetConnectAttr failed", ddbc_error=ddbc_error ), # Driver's SQLSetConnectAttr failed "IM007": OperationalError( driver_error="No data source or driver specified; dialog prohibited", ddbc_error=ddbc_error, ), # No data source or driver specified; dialog prohibited "IM008": OperationalError( - driver_error="Dialog failed", - ddbc_error=ddbc_error + driver_error="Dialog failed", ddbc_error=ddbc_error ), # Dialog failed "IM009": OperationalError( - driver_error="Unable to load translation DLL", - ddbc_error=ddbc_error + driver_error="Unable to load translation DLL", ddbc_error=ddbc_error ), # Unable to load translation DLL "IM010": OperationalError( - driver_error="Data source name too long", - ddbc_error=ddbc_error + driver_error="Data source name too long", ddbc_error=ddbc_error ), # Data source name too long "IM011": OperationalError( - driver_error="Driver name too long", - ddbc_error=ddbc_error + driver_error="Driver name too long", ddbc_error=ddbc_error ), # Driver name too long "IM012": OperationalError( - driver_error="DRIVER keyword syntax error", - ddbc_error=ddbc_error + driver_error="DRIVER keyword syntax error", ddbc_error=ddbc_error ), # DRIVER keyword syntax error "IM013": OperationalError( - driver_error="Trace file error", - ddbc_error=ddbc_error + driver_error="Trace file error", ddbc_error=ddbc_error ), # Trace file error "IM014": OperationalError( - driver_error="Invalid name of File DSN", - ddbc_error=ddbc_error + driver_error="Invalid name of File DSN", ddbc_error=ddbc_error ), # Invalid name of File DSN "IM015": OperationalError( - driver_error="Corrupt file data source", - ddbc_error=ddbc_error + driver_error="Corrupt file data source", ddbc_error=ddbc_error ), # Corrupt file data source } return mapping.get(sqlstate, None) @@ -622,7 +521,7 @@ def truncate_error_message(error_message: str) -> str: return string_first + string_third except Exception as e: if logger: - logger.error("Error while truncating error message: %s",e) + logger.error("Error while truncating error message: %s", e) return error_message @@ -646,5 +545,5 @@ def raise_exception(sqlstate: str, ddbc_error: str) -> None: raise exception_class raise DatabaseError( driver_error=f"An error occurred with SQLSTATE code: {sqlstate}", - ddbc_error=f"{ddbc_error}" if ddbc_error else f"Unknown DDBC error", + ddbc_error=f"{ddbc_error}" if ddbc_error else "Unknown DDBC error", ) diff --git a/mssql_python/helpers.py b/mssql_python/helpers.py index 2ac3c669..4c3b60b4 100644 --- a/mssql_python/helpers.py +++ b/mssql_python/helpers.py @@ -3,13 +3,11 @@ Licensed under the MIT license. This module provides helper functions for the mssql_python package. """ +import re from mssql_python import ddbc_bindings from mssql_python.exceptions import raise_exception from mssql_python.logging_config import get_logger -import platform -from pathlib import Path -from mssql_python.ddbc_bindings import normalize_architecture logger = get_logger() @@ -124,7 +122,7 @@ def sanitize_connection_string(conn_str: str) -> str: """ # Remove sensitive information from the connection string, Pwd section # Replace Pwd=...; or Pwd=... (end of string) with Pwd=***; - import re + return re.sub(r"(Pwd\s*=\s*)[^;]*", r"\1***", conn_str, flags=re.IGNORECASE) @@ -132,26 +130,24 @@ def sanitize_user_input(user_input: str, max_length: int = 50) -> str: """ Sanitize user input for safe logging by removing control characters, limiting length, and ensuring safe characters only. - + Args: user_input (str): The user input to sanitize. max_length (int): Maximum length of the sanitized output. - + Returns: str: The sanitized string safe for logging. """ if not isinstance(user_input, str): return "" - - # Remove control characters and non-printable characters - import re + # Allow alphanumeric, dash, underscore, and dot (common in encoding names) - sanitized = re.sub(r'[^\w\-\.]', '', user_input) - + sanitized = re.sub(r"[^\w\-\.]", "", user_input) + # Limit length to prevent log flooding if len(sanitized) > max_length: sanitized = sanitized[:max_length] + "..." - + # Return placeholder if nothing remains after sanitization return sanitized if sanitized else "" @@ -159,7 +155,7 @@ def sanitize_user_input(user_input: str, max_length: int = 50) -> str: def log(level: str, message: str, *args) -> None: """ Universal logging helper that gets a fresh logger instance. - + Args: level: Log level ('debug', 'info', 'warning', 'error') message: Log message with optional format placeholders @@ -167,4 +163,4 @@ def log(level: str, message: str, *args) -> None: """ logger = get_logger() if logger: - getattr(logger, level)(message, *args) \ No newline at end of file + getattr(logger, level)(message, *args) diff --git a/mssql_python/logging_config.py b/mssql_python/logging_config.py index 2e9eaaea..567127c0 100644 --- a/mssql_python/logging_config.py +++ b/mssql_python/logging_config.py @@ -17,38 +17,39 @@ class LoggingManager: This class provides a centralized way to manage logging configuration and replaces the previous approach using global variables. """ + _instance = None _initialized = False _logger = None _log_file = None - + def __new__(cls): if cls._instance is None: cls._instance = super(LoggingManager, cls).__new__(cls) return cls._instance - + def __init__(self): if not self._initialized: self._initialized = True self._enabled = False - + @classmethod def is_logging_enabled(cls): """Class method to check if logging is enabled for backward compatibility""" if cls._instance is None: return False return cls._instance._enabled - + @property def enabled(self): """Check if logging is enabled""" return self._enabled - + @property def log_file(self): """Get the current log file path""" return self._log_file - + def setup(self, mode="file", log_level=logging.DEBUG): """ Set up logging configuration. @@ -67,14 +68,14 @@ def setup(self, mode="file", log_level=logging.DEBUG): # Use a consistent logger name to ensure we're using the same logger throughout self._logger = logging.getLogger("mssql_python") self._logger.setLevel(log_level) - + # Configure the root logger to ensure all messages are captured root_logger = logging.getLogger() root_logger.setLevel(log_level) - + # Make sure the logger propagates to the root logger self._logger.propagate = True - + # Clear any existing handlers to avoid duplicates during re-initialization if self._logger.handlers: self._logger.handlers.clear() @@ -82,48 +83,61 @@ def setup(self, mode="file", log_level=logging.DEBUG): # Construct the path to the log file # Directory for log files - currentdir/logs current_dir = os.path.dirname(os.path.abspath(__file__)) - log_dir = os.path.join(current_dir, 'logs') + log_dir = os.path.join(current_dir, "logs") # exist_ok=True allows the directory to be created if it doesn't exist os.makedirs(log_dir, exist_ok=True) - + # Generate timestamp-based filename for better sorting and organization timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") - self._log_file = os.path.join(log_dir, f'mssql_python_trace_{timestamp}_{os.getpid()}.log') + self._log_file = os.path.join( + log_dir, f"mssql_python_trace_{timestamp}_{os.getpid()}.log" + ) # Create a log handler to log to driver specific file # By default we only want to log to a file, max size 500MB, and keep 5 backups - file_handler = RotatingFileHandler(self._log_file, maxBytes=512*1024*1024, backupCount=5) + file_handler = RotatingFileHandler( + self._log_file, maxBytes=512 * 1024 * 1024, backupCount=5 + ) file_handler.setLevel(log_level) - + # Create a custom formatter that adds [Python Layer log] prefix only to non-DDBC messages class PythonLayerFormatter(logging.Formatter): + """ + Custom formatter for Python Layer logs. + """ def format(self, record): message = record.getMessage() - # Don't add [Python Layer log] prefix if the message already has [DDBC Bindings log] or [Python Layer log] - if "[DDBC Bindings log]" not in message and "[Python Layer log]" not in message: + # Don't add [Python Layer log] prefix if the message already has + # [DDBC Bindings log] or [Python Layer log] + if ( + "[DDBC Bindings log]" not in message + and "[Python Layer log]" not in message + ): # Create a copy of the record to avoid modifying the original new_record = logging.makeLogRecord(record.__dict__) new_record.msg = f"[Python Layer log] {record.msg}" return super().format(new_record) return super().format(record) - + # Use our custom formatter - formatter = PythonLayerFormatter('%(asctime)s - %(levelname)s - %(filename)s - %(message)s') + formatter = PythonLayerFormatter( + "%(asctime)s - %(levelname)s - %(filename)s - %(message)s" + ) file_handler.setFormatter(formatter) self._logger.addHandler(file_handler) - if mode == 'stdout': + if mode == "stdout": # If the mode is stdout, then we want to log to the console as well stdout_handler = logging.StreamHandler(sys.stdout) stdout_handler.setLevel(log_level) # Use the same smart formatter stdout_handler.setFormatter(formatter) self._logger.addHandler(stdout_handler) - elif mode != 'file': - raise ValueError(f'Invalid logging mode: {mode}') - + elif mode != "file": + raise ValueError(f"Invalid logging mode: {mode}") + return self._logger - + def get_logger(self): """ Get the logger instance. @@ -140,25 +154,27 @@ def get_logger(self): # Create a singleton instance _manager = LoggingManager() + def setup_logging(mode="file", log_level=logging.DEBUG): """ Set up logging configuration. - + This is a wrapper around the LoggingManager.setup method for backward compatibility. - + Args: mode (str): The logging mode ('file' or 'stdout'). log_level (int): The logging level (default: logging.DEBUG). """ return _manager.setup(mode, log_level) + def get_logger(): """ Get the logger instance. - + This is a wrapper around the LoggingManager.get_logger method for backward compatibility. Returns: logging.Logger: The logger instance. """ - return _manager.get_logger() \ No newline at end of file + return _manager.get_logger() diff --git a/mssql_python/mssql_python.pyi b/mssql_python/mssql_python.pyi index 9f41d58d..9c1643eb 100644 --- a/mssql_python/mssql_python.pyi +++ b/mssql_python/mssql_python.pyi @@ -23,7 +23,7 @@ class STRING: class BINARY: """ - This type object is used to describe (long) + This type object is used to describe (long) binary columns in a database (e.g. LONG, RAW, BLOBs). """ @@ -54,7 +54,13 @@ class ROWID: def Date(year: int, month: int, day: int) -> datetime.date: ... def Time(hour: int, minute: int, second: int) -> datetime.time: ... def Timestamp( - year: int, month: int, day: int, hour: int, minute: int, second: int, microsecond: int + year: int, + month: int, + day: int, + hour: int, + minute: int, + second: int, + microsecond: int, ) -> datetime.datetime: ... def DateFromTicks(ticks: int) -> datetime.date: ... def TimeFromTicks(ticks: int) -> datetime.time: ... diff --git a/mssql_python/pooling.py b/mssql_python/pooling.py index 3658242a..551ef22f 100644 --- a/mssql_python/pooling.py +++ b/mssql_python/pooling.py @@ -1,19 +1,33 @@ -# mssql_python/pooling.py +""" +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. +This module provides a connection pooling manager for efficient database connections. +""" import atexit -from mssql_python import ddbc_bindings import threading +from mssql_python import ddbc_bindings class PoolingManager: + """ + Manages connection pooling for the application. + + This class provides methods to enable, disable, and + check the status of connection pooling. + """ _enabled = False - _initialized = False + _initialized = False _lock = threading.Lock() - _config = { - "max_size": 100, - "idle_timeout": 600 - } + _config = {"max_size": 100, "idle_timeout": 600} @classmethod def enable(cls, max_size=100, idle_timeout=600): + """ + Enable connection pooling with the specified parameters. + + Args: + max_size (int): The maximum number of connections in the pool. + idle_timeout (int): The idle timeout for connections in the pool (in seconds). + """ with cls._lock: if cls._enabled: return @@ -29,19 +43,32 @@ def enable(cls, max_size=100, idle_timeout=600): @classmethod def disable(cls): + """ + Disable connection pooling. + """ with cls._lock: cls._enabled = False cls._initialized = True @classmethod def is_enabled(cls): + """ + Check if connection pooling is enabled. + """ return cls._enabled @classmethod def is_initialized(cls): + """ + Check if connection pooling is initialized. + """ return cls._initialized - + + @atexit.register def shutdown_pooling(): + """ + Shutdown the connection pooling manager. + """ if PoolingManager.is_enabled(): ddbc_bindings.close_pooling() diff --git a/mssql_python/row.py b/mssql_python/row.py index bbea7fde..267cbe1c 100644 --- a/mssql_python/row.py +++ b/mssql_python/row.py @@ -1,18 +1,24 @@ +""" +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. +This module provides a Row class for representing a single row of data from a database query. +""" + class Row: """ A row of data from a cursor fetch operation. Provides both tuple-like indexing and attribute access to column values. - + Example: row = cursor.fetchone() print(row[0]) # Access by index print(row.column_name) # Access by column name """ - + def __init__(self, values, description, column_map=None): """ Initialize a Row object with values and description. - + Args: values: List of values for this row. description: Description of the columns (from cursor.description). @@ -20,27 +26,31 @@ def __init__(self, values, description, column_map=None): """ self._values = values self._description = description - + # Build column map if not provided if column_map is None: self._column_map = {} for i, desc in enumerate(description): col_name = desc[0] self._column_map[col_name] = i - self._column_map[col_name.lower()] = i # Add lowercase for case-insensitivity + self._column_map[col_name.lower()] = ( + i # Add lowercase for case-insensitivity + ) else: self._column_map = column_map - + def __getitem__(self, index): """Allow accessing by numeric index: row[0]""" return self._values[index] - + def __getattr__(self, name): """Allow accessing by column name as attribute: row.column_name""" if name in self._column_map: return self._values[self._column_map[name]] - raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'") - + raise AttributeError( + f"'{self.__class__.__name__}' object has no attribute '{name}'" + ) + def __eq__(self, other): """ Support comparison with lists for test compatibility. @@ -51,19 +61,19 @@ def __eq__(self, other): elif isinstance(other, Row): return self._values == other._values return super().__eq__(other) - + def __len__(self): """Return the number of values in the row""" return len(self._values) - + def __iter__(self): """Allow iteration through values""" return iter(self._values) - + def __str__(self): """Return string representation of the row""" return str(tuple(self._values)) def __repr__(self): """Return a detailed string representation for debugging""" - return repr(tuple(self._values)) \ No newline at end of file + return repr(tuple(self._values)) diff --git a/mssql_python/testing_ddbc_bindings.py b/mssql_python/testing_ddbc_bindings.py index a5aa2ae7..1325d12e 100644 --- a/mssql_python/testing_ddbc_bindings.py +++ b/mssql_python/testing_ddbc_bindings.py @@ -3,7 +3,7 @@ Licensed under the MIT license. This module provides functions to test DDBC bindings. """ -import ctypes + import datetime import os from mssql_python import ddbc_bindings @@ -26,10 +26,7 @@ def alloc_handle(handle_type, input_handle): """ Allocate a handle for the given handle type and input handle. """ - result_alloc, handle = ddbc_bindings.DDBCSQLAllocHandle( - handle_type, - input_handle - ) + result_alloc, handle = ddbc_bindings.DDBCSQLAllocHandle(handle_type, input_handle) if result_alloc < 0: print( "Error:", ddbc_bindings.DDBCSQLCheckError(handle_type, handle, result_alloc) @@ -62,7 +59,9 @@ def ddbc_sql_execute( if result_execute < 0: print( "Error: ", - ddbc_bindings.DDBCSQLCheckError(SQL_HANDLE_STMT, stmt_handle, result_execute), + ddbc_bindings.DDBCSQLCheckError( + SQL_HANDLE_STMT, stmt_handle, result_execute + ), ) raise RuntimeError(f"Failed to execute query. Error code: {result_execute}") return result_execute @@ -145,7 +144,9 @@ def fetch_data(stmt_handle): raise RuntimeError(f"Failed to fetch data. Error code: {result_fetch}") if column_count > 0: row = [] - result_get_data = ddbc_bindings.DDBCSQLGetData(stmt_handle, column_count, row) + result_get_data = ddbc_bindings.DDBCSQLGetData( + stmt_handle, column_count, row + ) if result_get_data < 0: print( "Error: ", @@ -167,7 +168,9 @@ def describe_columns(stmt_handle): if result_describe < 0: print( "Error: ", - ddbc_bindings.DDBCSQLCheckError(SQL_HANDLE_STMT, stmt_handle, result_describe), + ddbc_bindings.DDBCSQLCheckError( + SQL_HANDLE_STMT, stmt_handle, result_describe + ), ) raise RuntimeError(f"Failed to describe columns. Error code: {result_describe}") return column_names @@ -177,7 +180,9 @@ def connect_to_db(dbc_handle, connection_string): """ Connect to the database using DDBC bindings. """ - result_connect = ddbc_bindings.DDBCSQLDriverConnect(dbc_handle, 0, connection_string) + result_connect = ddbc_bindings.DDBCSQLDriverConnect( + dbc_handle, 0, connection_string + ) if result_connect < 0: print( "Error: ", @@ -346,7 +351,7 @@ def add_numeric_param(params, param_infos, param): if __name__ == "__main__": # Allocate environment handle env_handle = alloc_handle(SQL_HANDLE_ENV, None) - + # Set the DDBC version environment attribute result_set_env = ddbc_bindings.DDBCSQLSetEnvAttr( env_handle, SQL_ATTR_DDBC_VERSION, SQL_OV_DDBC3_80, 0 @@ -410,7 +415,12 @@ def add_numeric_param(params, param_infos, param): # add_numeric_param(params_insert, param_info_list_insert, decimal.Decimal('12')) is_stmt_prepared_insert = [False] result_insert = ddbc_sql_execute( - stmt_handle, insert_sql_query, params_insert, param_info_list_insert, is_stmt_prepared_insert, True + stmt_handle, + insert_sql_query, + params_insert, + param_info_list_insert, + is_stmt_prepared_insert, + True, ) print("DDBCSQLExecute result:", result_insert) @@ -424,7 +434,12 @@ def add_numeric_param(params, param_infos, param): params_select = [] param_info_list_select = [] result_select = ddbc_sql_execute( - stmt_handle, select_sql_query, params_select, param_info_list_select, is_stmt_prepared_select, False + stmt_handle, + select_sql_query, + params_select, + param_info_list_select, + is_stmt_prepared_select, + False, ) print("DDBCSQLExecute result:", result_select) @@ -448,7 +463,9 @@ def add_numeric_param(params, param_infos, param): if result_disconnect < 0: print( "Error: ", - ddbc_bindings.DDBCSQLCheckError(SQL_HANDLE_DBC, dbc_handle, result_disconnect), + ddbc_bindings.DDBCSQLCheckError( + SQL_HANDLE_DBC, dbc_handle, result_disconnect + ), ) raise RuntimeError( f"Failed to disconnect from the data source. Error code: {result_disconnect}" diff --git a/mssql_python/type.py b/mssql_python/type.py index 69ecf251..7963631d 100644 --- a/mssql_python/type.py +++ b/mssql_python/type.py @@ -71,7 +71,13 @@ def Time(hour: int, minute: int, second: int) -> datetime.time: def Timestamp( - year: int, month: int, day: int, hour: int, minute: int, second: int, microsecond: int + year: int, + month: int, + day: int, + hour: int, + minute: int, + second: int, + microsecond: int, ) -> datetime.datetime: """ Generates a timestamp object. @@ -103,22 +109,22 @@ def TimestampFromTicks(ticks: int) -> datetime.datetime: def Binary(value) -> bytes: """ Converts a string or bytes to bytes for use with binary database columns. - + This function follows the DB-API 2.0 specification. It accepts only str and bytes/bytearray types to ensure type safety. - + Args: value: A string (str) or bytes-like object (bytes, bytearray) - + Returns: bytes: The input converted to bytes - + Raises: TypeError: If the input type is not supported - + Examples: Binary("hello") # Returns b"hello" - Binary(b"hello") # Returns b"hello" + Binary(b"hello") # Returns b"hello" Binary(bytearray(b"hi")) # Returns b"hi" """ if isinstance(value, bytes): @@ -129,5 +135,7 @@ def Binary(value) -> bytes: return value.encode("utf-8") else: # Raise TypeError for unsupported types to improve type safety - raise TypeError(f"Cannot convert type {type(value).__name__} to bytes. " - f"Binary() only accepts str, bytes, or bytearray objects.") + raise TypeError( + f"Cannot convert type {type(value).__name__} to bytes. " + f"Binary() only accepts str, bytes, or bytearray objects." + ) From 00401a069606acdcd7ddc173b8b4d9836d86e247 Mon Sep 17 00:00:00 2001 From: Jahnvi Thakkar Date: Wed, 3 Sep 2025 12:30:50 +0530 Subject: [PATCH 02/20] FIX: Python Code linting --- tests/test_004_cursor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index 9caa9114..bbe2f886 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -4874,7 +4874,7 @@ def test_tables_with_type_filter(cursor, db_connection): # Get only tables tables_list = cursor.tables( schema='pytest_tables_schema', - tableType='TABLE' + table_type='TABLE' ) # Verify only regular tables @@ -4895,7 +4895,7 @@ def test_tables_with_type_filter(cursor, db_connection): # Get only views views_list = cursor.tables( schema='pytest_tables_schema', - tableType='VIEW' + table_type='VIEW' ) # Verify only views @@ -4918,7 +4918,7 @@ def test_tables_with_multiple_types(cursor, db_connection): # Get both tables and views tables_list = cursor.tables( schema='pytest_tables_schema', - tableType=['TABLE', 'VIEW'] + table_type=['TABLE', 'VIEW'] ) # Verify both tables and views @@ -5002,7 +5002,7 @@ def test_tables_combined_filters(cursor, db_connection): tables_list = cursor.tables( schema='pytest_tables_schema', table='%table', - tableType='TABLE' + table_type='TABLE' ) # Should find both tables but not view From 939252de5d3234bc1113901dc9e6c0f0826601df Mon Sep 17 00:00:00 2001 From: Jahnvi Thakkar Date: Tue, 9 Sep 2025 00:15:21 +0530 Subject: [PATCH 03/20] Cpp linting --- mssql_python/pybind/connection/connection.cpp | 141 +- mssql_python/pybind/connection/connection.h | 20 +- .../pybind/connection/connection_pool.cpp | 62 +- .../pybind/connection/connection_pool.h | 42 +- mssql_python/pybind/ddbc_bindings.cpp | 2145 +++++++++++------ mssql_python/pybind/ddbc_bindings.h | 327 ++- mssql_python/pybind/unix_buffers.h | 72 +- mssql_python/pybind/unix_utils.cpp | 40 +- mssql_python/pybind/unix_utils.h | 11 +- 9 files changed, 1870 insertions(+), 990 deletions(-) diff --git a/mssql_python/pybind/connection/connection.cpp b/mssql_python/pybind/connection/connection.cpp index 9782efd2..33857c66 100644 --- a/mssql_python/pybind/connection/connection.cpp +++ b/mssql_python/pybind/connection/connection.cpp @@ -1,15 +1,21 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -// INFO|TODO - Note that is file is Windows specific right now. Making it arch agnostic will be +// INFO|TODO - Note that is file is Windows specific right now. +// Making it arch agnostic will be // taken up in future -#include "connection.h" -#include "connection_pool.h" -#include +#include "connection/connection.h" + #include -#define SQL_COPT_SS_ACCESS_TOKEN 1256 // Custom attribute ID for access token +#include +#include // for string +#include // for make_shared<> + +#include "connection/connection_pool.h" + +#define SQL_COPT_SS_ACCESS_TOKEN 1256 // Custom attribute ID for access token static SqlHandlePtr getEnvHandle() { static SqlHandlePtr envHandle = []() -> SqlHandlePtr { @@ -19,15 +25,23 @@ static SqlHandlePtr getEnvHandle() { DriverLoader::getInstance().loadDriver(); } SQLHANDLE env = nullptr; - SQLRETURN ret = SQLAllocHandle_ptr(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env); + SQLRETURN ret = SQLAllocHandle_ptr( + SQL_HANDLE_ENV, + SQL_NULL_HANDLE, + &env); if (!SQL_SUCCEEDED(ret)) { ThrowStdException("Failed to allocate environment handle"); } - ret = SQLSetEnvAttr_ptr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3_80, 0); + ret = SQLSetEnvAttr_ptr(env, + SQL_ATTR_ODBC_VERSION, + (void*)SQL_OV_ODBC3_80, + 0); if (!SQL_SUCCEEDED(ret)) { ThrowStdException("Failed to set environment attributes"); } - return std::make_shared(static_cast(SQL_HANDLE_ENV), env); + return std::make_shared( + static_cast(SQL_HANDLE_ENV), + env); }(); return envHandle; @@ -44,7 +58,7 @@ Connection::Connection(const std::wstring& conn_str, bool use_pool) } Connection::~Connection() { - disconnect(); // fallback if user forgets to disconnect + disconnect(); // fallback if user forgets to disconnect } // Allocates connection handle @@ -54,7 +68,9 @@ void Connection::allocateDbcHandle() { LOG("Allocate SQL Connection Handle"); SQLRETURN ret = SQLAllocHandle_ptr(SQL_HANDLE_DBC, _envHandle->get(), &dbc); checkError(ret); - _dbcHandle = std::make_shared(static_cast(SQL_HANDLE_DBC), dbc); + _dbcHandle = std::make_shared( + static_cast(SQL_HANDLE_DBC), + dbc); } void Connection::connect(const py::dict& attrs_before) { @@ -68,7 +84,7 @@ void Connection::connect(const py::dict& attrs_before) { } } SQLWCHAR* connStrPtr; -#if defined(__APPLE__) || defined(__linux__) // macOS/Linux specific handling +#if defined(__APPLE__) || defined(__linux__) // macOS/Linux specific handling LOG("Creating connection string buffer for macOS/Linux"); std::vector connStrBuffer = WStringToSQLWCHAR(_connStr); // Ensure the buffer is null-terminated @@ -78,10 +94,9 @@ void Connection::connect(const py::dict& attrs_before) { #else connStrPtr = const_cast(_connStr.c_str()); #endif - SQLRETURN ret = SQLDriverConnect_ptr( - _dbcHandle->get(), nullptr, - connStrPtr, SQL_NTS, - nullptr, 0, nullptr, SQL_DRIVER_NOPROMPT); + SQLRETURN ret = SQLDriverConnect_ptr(_dbcHandle->get(), nullptr, + connStrPtr, SQL_NTS, nullptr, + 0, nullptr, SQL_DRIVER_NOPROMPT); checkError(ret); updateLastUsed(); } @@ -91,15 +106,15 @@ void Connection::disconnect() { LOG("Disconnecting from database"); SQLRETURN ret = SQLDisconnect_ptr(_dbcHandle->get()); checkError(ret); - _dbcHandle.reset(); // triggers SQLFreeHandle via destructor, if last owner - } - else { + // triggers SQLFreeHandle via destructor, if last owner + _dbcHandle.reset(); + } else { LOG("No connection handle to disconnect"); } } // TODO: Add an exception class in C++ for error handling, DB spec compliant -void Connection::checkError(SQLRETURN ret) const{ +void Connection::checkError(SQLRETURN ret) const { if (!SQL_SUCCEEDED(ret)) { ErrorInfo err = SQLCheckError_Wrap(SQL_HANDLE_DBC, _dbcHandle, ret); std::string errorMsg = WideToUTF8(err.ddbcErrorMsg); @@ -109,33 +124,43 @@ void Connection::checkError(SQLRETURN ret) const{ void Connection::commit() { if (!_dbcHandle) { - ThrowStdException("Connection handle not allocated"); + ThrowStdException("Connection object is not initialized"); } updateLastUsed(); LOG("Committing transaction"); - SQLRETURN ret = SQLEndTran_ptr(SQL_HANDLE_DBC, _dbcHandle->get(), SQL_COMMIT); + SQLRETURN ret = SQLEndTran_ptr(SQL_HANDLE_DBC, + _dbcHandle->get(), + SQL_COMMIT); checkError(ret); } void Connection::rollback() { if (!_dbcHandle) { - ThrowStdException("Connection handle not allocated"); + ThrowStdException("Connection object is not initialized"); } updateLastUsed(); LOG("Rolling back transaction"); - SQLRETURN ret = SQLEndTran_ptr(SQL_HANDLE_DBC, _dbcHandle->get(), SQL_ROLLBACK); + SQLRETURN ret = SQLEndTran_ptr(SQL_HANDLE_DBC, + _dbcHandle->get(), + SQL_ROLLBACK); checkError(ret); } void Connection::setAutocommit(bool enable) { if (!_dbcHandle) { - ThrowStdException("Connection handle not allocated"); + ThrowStdException("Connection object is not initialized"); } SQLINTEGER value = enable ? SQL_AUTOCOMMIT_ON : SQL_AUTOCOMMIT_OFF; LOG("Setting SQL Connection Attribute"); - SQLRETURN ret = SQLSetConnectAttr_ptr(_dbcHandle->get(), SQL_ATTR_AUTOCOMMIT, reinterpret_cast(static_cast(value)), 0); + SQLRETURN ret = + SQLSetConnectAttr_ptr( + _dbcHandle->get(), + SQL_ATTR_AUTOCOMMIT, + reinterpret_cast( + static_cast(value)), + 0); checkError(ret); - if(value == SQL_AUTOCOMMIT_ON) { + if (value == SQL_AUTOCOMMIT_ON) { LOG("SQL Autocommit set to True"); } else { LOG("SQL Autocommit set to False"); @@ -145,29 +170,34 @@ void Connection::setAutocommit(bool enable) { bool Connection::getAutocommit() const { if (!_dbcHandle) { - ThrowStdException("Connection handle not allocated"); + ThrowStdException("Connection object is not initialized"); } LOG("Get SQL Connection Attribute"); SQLINTEGER value; SQLINTEGER string_length; - SQLRETURN ret = SQLGetConnectAttr_ptr(_dbcHandle->get(), SQL_ATTR_AUTOCOMMIT, &value, sizeof(value), &string_length); + SQLRETURN ret = SQLGetConnectAttr_ptr(_dbcHandle->get(), + SQL_ATTR_AUTOCOMMIT, &value, + sizeof(value), &string_length); checkError(ret); return value == SQL_AUTOCOMMIT_ON; } SqlHandlePtr Connection::allocStatementHandle() { if (!_dbcHandle) { - ThrowStdException("Connection handle not allocated"); + ThrowStdException("Connection object is not initialized"); } updateLastUsed(); LOG("Allocating statement handle"); SQLHANDLE stmt = nullptr; - SQLRETURN ret = SQLAllocHandle_ptr(SQL_HANDLE_STMT, _dbcHandle->get(), &stmt); + SQLRETURN ret = SQLAllocHandle_ptr(SQL_HANDLE_STMT, + _dbcHandle->get(), + &stmt); checkError(ret); - return std::make_shared(static_cast(SQL_HANDLE_STMT), stmt); + return std::make_shared( + static_cast(SQL_HANDLE_STMT), + stmt); } - SQLRETURN Connection::setAttribute(SQLINTEGER attribute, py::object value) { LOG("Setting SQL attribute"); SQLPOINTER ptr = nullptr; @@ -177,7 +207,8 @@ SQLRETURN Connection::setAttribute(SQLINTEGER attribute, py::object value) { int intValue = value.cast(); ptr = reinterpret_cast(static_cast(intValue)); length = SQL_IS_INTEGER; - } else if (py::isinstance(value) || py::isinstance(value)) { + } else if (py::isinstance(value) || + py::isinstance(value)) { static std::vector buffers; buffers.emplace_back(value.cast()); ptr = const_cast(buffers.back().c_str()); @@ -187,11 +218,13 @@ SQLRETURN Connection::setAttribute(SQLINTEGER attribute, py::object value) { return SQL_ERROR; } - SQLRETURN ret = SQLSetConnectAttr_ptr(_dbcHandle->get(), attribute, ptr, length); + SQLRETURN ret = SQLSetConnectAttr_ptr(_dbcHandle->get(), + attribute, + ptr, + length); if (!SQL_SUCCEEDED(ret)) { LOG("Failed to set attribute"); - } - else { + } else { LOG("Set attribute successfully"); } return ret; @@ -206,8 +239,10 @@ void Connection::applyAttrsBefore(const py::dict& attrs) { continue; } - if (key == SQL_COPT_SS_ACCESS_TOKEN) { - SQLRETURN ret = setAttribute(key, py::reinterpret_borrow(item.second)); + if (key == SQL_COPT_SS_ACCESS_TOKEN) { + SQLRETURN ret = setAttribute( + key, + py::reinterpret_borrow(item.second)); if (!SQL_SUCCEEDED(ret)) { ThrowStdException("Failed to set access token before connect"); } @@ -217,24 +252,27 @@ void Connection::applyAttrsBefore(const py::dict& attrs) { bool Connection::isAlive() const { if (!_dbcHandle) { - ThrowStdException("Connection handle not allocated"); + ThrowStdException("Connection object is not initialized"); } SQLUINTEGER status; - SQLRETURN ret = SQLGetConnectAttr_ptr(_dbcHandle->get(), SQL_ATTR_CONNECTION_DEAD, - &status, 0, nullptr); + SQLRETURN ret = + SQLGetConnectAttr_ptr(_dbcHandle->get(), + SQL_ATTR_CONNECTION_DEAD, + &status, + 0, + nullptr); return SQL_SUCCEEDED(ret) && status == SQL_CD_FALSE; } bool Connection::reset() { if (!_dbcHandle) { - ThrowStdException("Connection handle not allocated"); + ThrowStdException("Connection object is not initialized"); } LOG("Resetting connection via SQL_ATTR_RESET_CONNECTION"); - SQLRETURN ret = SQLSetConnectAttr_ptr( - _dbcHandle->get(), - SQL_ATTR_RESET_CONNECTION, - (SQLPOINTER)SQL_RESET_CONNECTION_YES, - SQL_IS_INTEGER); + SQLRETURN ret = SQLSetConnectAttr_ptr(_dbcHandle->get(), + SQL_ATTR_RESET_CONNECTION, + (SQLPOINTER)SQL_RESET_CONNECTION_YES, + SQL_IS_INTEGER); if (!SQL_SUCCEEDED(ret)) { LOG("Failed to reset connection. Marking as dead."); disconnect(); @@ -252,11 +290,14 @@ std::chrono::steady_clock::time_point Connection::lastUsed() const { return _lastUsed; } -ConnectionHandle::ConnectionHandle(const std::string& connStr, bool usePool, const py::dict& attrsBefore) +ConnectionHandle::ConnectionHandle(const std::string& connStr, + bool usePool, + const py::dict& attrsBefore) : _usePool(usePool) { _connStr = Utf8ToWString(connStr); if (_usePool) { - _conn = ConnectionPoolManager::getInstance().acquireConnection(_connStr, attrsBefore); + _conn = ConnectionPoolManager::getInstance().acquireConnection( + _connStr, attrsBefore); } else { _conn = std::make_shared(_connStr, false); _conn->connect(attrsBefore); @@ -314,4 +355,4 @@ SqlHandlePtr ConnectionHandle::allocStatementHandle() { ThrowStdException("Connection object is not initialized"); } return _conn->allocStatementHandle(); -} \ No newline at end of file +} diff --git a/mssql_python/pybind/connection/connection.h b/mssql_python/pybind/connection/connection.h index 6129125e..69845363 100644 --- a/mssql_python/pybind/connection/connection.h +++ b/mssql_python/pybind/connection/connection.h @@ -1,18 +1,21 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -// INFO|TODO - Note that is file is Windows specific right now. Making it arch agnostic will be +// INFO|TODO - Note that is file is Windows specific right now. +// Making it arch agnostic will be // taken up in future. #pragma once -#include "ddbc_bindings.h" +#include +#include +#include "../ddbc_bindings.h" // Represents a single ODBC database connection. // Manages connection handles. // Note: This class does NOT implement pooling logic directly. class Connection { -public: + public: Connection(const std::wstring& connStr, bool fromPool); ~Connection(); @@ -42,7 +45,7 @@ class Connection { // Allocate a new statement handle on this connection. SqlHandlePtr allocStatementHandle(); -private: + private: void allocateDbcHandle(); void checkError(SQLRETURN ret) const; SQLRETURN setAttribute(SQLINTEGER attribute, py::object value); @@ -56,8 +59,9 @@ class Connection { }; class ConnectionHandle { -public: - ConnectionHandle(const std::string& connStr, bool usePool, const py::dict& attrsBefore = py::dict()); + public: + ConnectionHandle(const std::string& connStr, bool usePool, + const py::dict& attrsBefore = py::dict()); ~ConnectionHandle(); void close(); @@ -67,8 +71,8 @@ class ConnectionHandle { bool getAutocommit() const; SqlHandlePtr allocStatementHandle(); -private: + private: std::shared_ptr _conn; bool _usePool; std::wstring _connStr; -}; \ No newline at end of file +}; diff --git a/mssql_python/pybind/connection/connection_pool.cpp b/mssql_python/pybind/connection/connection_pool.cpp index 60dd5415..e684efd4 100644 --- a/mssql_python/pybind/connection/connection_pool.cpp +++ b/mssql_python/pybind/connection/connection_pool.cpp @@ -1,16 +1,23 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -// INFO|TODO - Note that is file is Windows specific right now. Making it arch agnostic will be -// taken up in future. +// INFO|TODO - Note that is file is Windows specific right now. Making it arch +// agnostic will be taken up in future. + +#include "connection/connection_pool.h" -#include "connection_pool.h" #include +#include +#include ConnectionPool::ConnectionPool(size_t max_size, int idle_timeout_secs) - : _max_size(max_size), _idle_timeout_secs(idle_timeout_secs), _current_size(0) {} + : _max_size(max_size), + _idle_timeout_secs(idle_timeout_secs), + _current_size(0) {} -std::shared_ptr ConnectionPool::acquire(const std::wstring& connStr, const py::dict& attrs_before) { +std::shared_ptr ConnectionPool::acquire( + const std::wstring& connStr, + const py::dict& attrs_before) { std::vector> to_disconnect; std::shared_ptr valid_conn = nullptr; { @@ -19,18 +26,26 @@ std::shared_ptr ConnectionPool::acquire(const std::wstring& connStr, size_t before = _pool.size(); // Phase 1: Remove stale connections, collect for later disconnect - _pool.erase(std::remove_if(_pool.begin(), _pool.end(), - [&](const std::shared_ptr& conn) { - auto idle_time = std::chrono::duration_cast(now - conn->lastUsed()).count(); - if (idle_time > _idle_timeout_secs) { - to_disconnect.push_back(conn); - return true; - } - return false; - }), _pool.end()); + _pool.erase( + std::remove_if( + _pool.begin(), _pool.end(), + [&](const std::shared_ptr& conn) { + auto idle_time = + std::chrono::duration_cast( + now - conn->lastUsed()) + .count(); + if (idle_time > _idle_timeout_secs) { + to_disconnect.push_back(conn); + return true; + } + return false; + }), + _pool.end()); size_t pruned = before - _pool.size(); - _current_size = (_current_size >= pruned) ? (_current_size - pruned) : 0; + _current_size = (_current_size >= pruned) + ? (_current_size - pruned) + : 0; // Phase 2: Attempt to reuse healthy connections while (!_pool.empty()) { @@ -56,7 +71,8 @@ std::shared_ptr ConnectionPool::acquire(const std::wstring& connStr, valid_conn->connect(attrs_before); ++_current_size; } else if (!valid_conn) { - throw std::runtime_error("ConnectionPool::acquire: pool size limit reached"); + throw std::runtime_error( + "ConnectionPool::acquire: pool size limit reached"); } } @@ -76,8 +92,7 @@ void ConnectionPool::release(std::shared_ptr conn) { if (_pool.size() < _max_size) { conn->updateLastUsed(); _pool.push_back(conn); - } - else { + } else { conn->disconnect(); if (_current_size > 0) --_current_size; } @@ -107,18 +122,23 @@ ConnectionPoolManager& ConnectionPoolManager::getInstance() { return manager; } -std::shared_ptr ConnectionPoolManager::acquireConnection(const std::wstring& connStr, const py::dict& attrs_before) { +std::shared_ptr ConnectionPoolManager::acquireConnection( + const std::wstring& connStr, + const py::dict& attrs_before) { std::lock_guard lock(_manager_mutex); auto& pool = _pools[connStr]; if (!pool) { LOG("Creating new connection pool"); - pool = std::make_shared(_default_max_size, _default_idle_secs); + pool = std::make_shared( + _default_max_size, _default_idle_secs); } return pool->acquire(connStr, attrs_before); } -void ConnectionPoolManager::returnConnection(const std::wstring& conn_str, const std::shared_ptr conn) { +void ConnectionPoolManager::returnConnection( + const std::wstring& conn_str, + const std::shared_ptr conn) { std::lock_guard lock(_manager_mutex); if (_pools.find(conn_str) != _pools.end()) { _pools[conn_str]->release((conn)); diff --git a/mssql_python/pybind/connection/connection_pool.h b/mssql_python/pybind/connection/connection_pool.h index dc2de5a8..7a7a7b2e 100644 --- a/mssql_python/pybind/connection/connection_pool.h +++ b/mssql_python/pybind/connection/connection_pool.h @@ -5,21 +5,25 @@ // taken up in future. #pragma once +#include #include -#include #include #include #include -#include -#include "connection.h" +#include + +#include "connection/connection.h" -// Manages a fixed-size pool of reusable database connections for a single connection string +// Manages a fixed-size pool of reusable database connections for a single +// connection string class ConnectionPool { -public: + public: ConnectionPool(size_t max_size, int idle_timeout_secs); // Acquires a connection from the pool or creates a new one if under limit - std::shared_ptr acquire(const std::wstring& connStr, const py::dict& attrs_before = py::dict()); + std::shared_ptr acquire( + const std::wstring& connStr, + const py::dict& attrs_before = py::dict()); // Returns a connection to the pool for reuse void release(std::shared_ptr conn); @@ -27,43 +31,47 @@ class ConnectionPool { // Closes all connections in the pool, releasing resources void close(); -private: - size_t _max_size; // Maximum number of connections allowed - int _idle_timeout_secs; // Idle time before connections are considered stale + private: + size_t _max_size; // Maximum number of connections allowed + int _idle_timeout_secs; // Idle time before connections are considered stale size_t _current_size = 0; std::deque> _pool; // Available connections - std::mutex _mutex; // Mutex for thread-safe access + std::mutex _mutex; // Mutex for thread-safe access }; // Singleton manager that handles multiple pools keyed by connection string class ConnectionPoolManager { -public: + public: // Returns the singleton instance of the manager static ConnectionPoolManager& getInstance(); void configure(int max_size, int idle_timeout); // Gets a connection from the appropriate pool (creates one if none exists) - std::shared_ptr acquireConnection(const std::wstring& conn_str, const py::dict& attrs_before = py::dict()); + std::shared_ptr acquireConnection( + const std::wstring& conn_str, + const py::dict& attrs_before = py::dict()); // Returns a connection to its original pool - void returnConnection(const std::wstring& conn_str, std::shared_ptr conn); + void returnConnection(const std::wstring& conn_str, + std::shared_ptr conn); // Closes all pools and their connections void closePools(); -private: - ConnectionPoolManager() = default; + private: + ConnectionPoolManager() = default; ~ConnectionPoolManager() = default; // Map from connection string to connection pool - std::unordered_map> _pools; + std::unordered_map> _pools; // Protects access to the _pools map std::mutex _manager_mutex; size_t _default_max_size = 10; int _default_idle_secs = 300; - + // Prevent copying ConnectionPoolManager(const ConnectionPoolManager&) = delete; ConnectionPoolManager& operator=(const ConnectionPoolManager&) = delete; diff --git a/mssql_python/pybind/ddbc_bindings.cpp b/mssql_python/pybind/ddbc_bindings.cpp index d457e9cc..3e76f18d 100644 --- a/mssql_python/pybind/ddbc_bindings.cpp +++ b/mssql_python/pybind/ddbc_bindings.cpp @@ -1,17 +1,22 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -// INFO|TODO - Note that is file is Windows specific right now. Making it arch agnostic will be -// taken up in beta release -#include "ddbc_bindings.h" -#include "connection/connection.h" -#include "connection/connection_pool.h" - +// INFO|TODO - Note that is file is Windows specific right now. +// Making it arch agnostic will be taken up in beta release #include #include // std::setw, std::setfill #include #include // std::forward -#include +#include // std::numeric_limits +#include // for snprintf +#include // for std::min +#include // for std::vector +#include // for std::unique_ptr +#include // for std::string + +#include "ddbc_bindings.h" +#include "connection/connection.h" +#include "connection/connection_pool.h" //------------------------------------------------------------------------------------------------- // Macro definitions @@ -28,14 +33,16 @@ // Architecture-specific defines #ifndef ARCHITECTURE -#define ARCHITECTURE "win64" // Default to win64 if not defined during compilation +// Default to win64 if not defined during compilation +#define ARCHITECTURE "win64" #endif #define DAE_CHUNK_SIZE 8192 //------------------------------------------------------------------------------------------------- // Class definitions //------------------------------------------------------------------------------------------------- -// Struct to hold parameter information for binding. Used by SQLBindParameter. +// Struct to hold parameter information for binding. +// Used by SQLBindParameter. // This struct is shared between C++ & Python code. struct ParamInfo { SQLSMALLINT inputOutputType; @@ -43,9 +50,9 @@ struct ParamInfo { SQLSMALLINT paramSQLType; SQLULEN columnSize; SQLSMALLINT decimalDigits; - SQLLEN strLenOrInd = 0; // Required for DAE - bool isDAE = false; // Indicates if we need to stream - py::object dataPtr; + SQLLEN strLenOrInd = 0; // Required for DAE + bool isDAE = false; // Indicates if we need to stream + py::object dataPtr; }; // Mirrors the SQL_NUMERIC_STRUCT. But redefined to replace val char array @@ -59,8 +66,11 @@ struct NumericData { NumericData() : precision(0), scale(0), sign(0), val(0) {} - NumericData(SQLCHAR precision, SQLSCHAR scale, SQLCHAR sign, std::uint64_t value) - : precision(precision), scale(scale), sign(sign), val(value) {} + NumericData(SQLCHAR precision, + SQLSCHAR scale, SQLCHAR sign, + std::uint64_t value) + : precision(precision), scale(scale), + sign(sign), val(value) {} }; // Struct to hold data buffers and indicators for each column @@ -90,7 +100,8 @@ struct ColumnBuffers { dateBuffers(numCols), timeBuffers(numCols), guidBuffers(numCols), - indicators(numCols, std::vector(fetchSize)) {} + indicators(numCols, + std::vector(fetchSize)) {} }; //------------------------------------------------------------------------------------------------- @@ -174,28 +185,39 @@ const char* GetSqlCTypeAsString(const SQLSMALLINT cType) { } } -std::string MakeParamMismatchErrorStr(const SQLSMALLINT cType, const int paramIndex) { +std::string MakeParamMismatchErrorStr( + const SQLSMALLINT cType, const int paramIndex) { std::string errorString = - "Parameter's object type does not match parameter's C type. paramIndex - " + - std::to_string(paramIndex) + ", C type - " + GetSqlCTypeAsString(cType); + "Parameter's object type does not match " + "parameter's C type. paramIndex - " + + std::to_string(paramIndex) + ", C type - " + + GetSqlCTypeAsString(cType); return errorString; } -// This function allocates a buffer of ParamType, stores it as a void* in paramBuffers for -// book-keeping and then returns a ParamType* to the allocated memory. -// ctorArgs are the arguments to ParamType's constructor used while creating/allocating ParamType +// This function allocates a buffer of ParamType, +// stores it as a void* in paramBuffers for +// book-keeping and then returns a ParamType* +// to the allocated memory. +// ctorArgs are the arguments to ParamType's constructor +// used while creating/allocating ParamType template -ParamType* AllocateParamBuffer(std::vector>& paramBuffers, - CtorArgs&&... ctorArgs) { - paramBuffers.emplace_back(new ParamType(std::forward(ctorArgs)...), +ParamType* AllocateParamBuffer( + std::vector>& paramBuffers, + CtorArgs&&... ctorArgs) { + paramBuffers.emplace_back( + new ParamType(std::forward(ctorArgs)...), std::default_delete()); return static_cast(paramBuffers.back().get()); } template -ParamType* AllocateParamBufferArray(std::vector>& paramBuffers, - size_t count) { - std::shared_ptr buffer(new ParamType[count], std::default_delete()); +ParamType* AllocateParamBufferArray( + std::vector>& paramBuffers, + size_t count) { + std::shared_ptr buffer( + new ParamType[count], + std::default_delete()); ParamType* raw = buffer.get(); paramBuffers.push_back(buffer); return raw; @@ -211,7 +233,8 @@ std::string DescribeChar(unsigned char ch) { } } -// Given a list of parameters and their ParamInfo, calls SQLBindParameter on each of them with +// Given a list of parameters and their ParamInfo, +// calls SQLBindParameter on each of them with // appropriate arguments SQLRETURN BindParameters(SQLHANDLE hStmt, const py::list& params, std::vector& paramInfos, @@ -220,7 +243,8 @@ SQLRETURN BindParameters(SQLHANDLE hStmt, const py::list& params, for (int paramIndex = 0; paramIndex < params.size(); paramIndex++) { const auto& param = params[paramIndex]; ParamInfo& paramInfo = paramInfos[paramIndex]; - LOG("Binding parameter {} - C Type: {}, SQL Type: {}", paramIndex, paramInfo.paramCType, paramInfo.paramSQLType); + LOG("Binding parameter {} - C Type: {}, SQL Type: {}", + paramIndex, paramInfo.paramCType, paramInfo.paramSQLType); void* dataPtr = nullptr; SQLLEN bufferLength = 0; SQLLEN* strLenOrIndPtr = nullptr; @@ -228,20 +252,32 @@ SQLRETURN BindParameters(SQLHANDLE hStmt, const py::list& params, // TODO: Add more data types like money, guid, interval, TVPs etc. switch (paramInfo.paramCType) { case SQL_C_CHAR: { - if (!py::isinstance(param) && !py::isinstance(param) && + if (!py::isinstance(param) && + !py::isinstance(param) && !py::isinstance(param)) { - ThrowStdException(MakeParamMismatchErrorStr(paramInfo.paramCType, paramIndex)); + ThrowStdException( + MakeParamMismatchErrorStr( + paramInfo.paramCType, + paramIndex)); } if (paramInfo.isDAE) { - LOG("Parameter[{}] is marked for DAE streaming", paramIndex); - dataPtr = const_cast(reinterpret_cast(¶mInfos[paramIndex])); + LOG("Parameter[{}] is marked for DAE streaming", + paramIndex); + dataPtr = + const_cast( + reinterpret_cast( + ¶mInfos[paramIndex])); strLenOrIndPtr = AllocateParamBuffer(paramBuffers); *strLenOrIndPtr = SQL_LEN_DATA_AT_EXEC(0); bufferLength = 0; } else { std::string* strParam = - AllocateParamBuffer(paramBuffers, param.cast()); - dataPtr = const_cast(static_cast(strParam->c_str())); + AllocateParamBuffer( + paramBuffers, + param.cast()); + dataPtr = const_cast( + static_cast( + strParam->c_str())); bufferLength = strParam->size() + 1; strLenOrIndPtr = AllocateParamBuffer(paramBuffers); *strLenOrIndPtr = SQL_NTS; @@ -249,42 +285,62 @@ SQLRETURN BindParameters(SQLHANDLE hStmt, const py::list& params, break; } case SQL_C_BINARY: { - if (!py::isinstance(param) && !py::isinstance(param) && + if (!py::isinstance(param) && + !py::isinstance(param) && !py::isinstance(param)) { - ThrowStdException(MakeParamMismatchErrorStr(paramInfo.paramCType, paramIndex)); + ThrowStdException( + MakeParamMismatchErrorStr( + paramInfo.paramCType, + paramIndex)); } std::string* strParam = - AllocateParamBuffer(paramBuffers, param.cast()); + AllocateParamBuffer( + paramBuffers, + param.cast()); if (strParam->size() > 8192 /* TODO: Fix max length */) { ThrowStdException( - "Streaming parameters is not yet supported. Parameter size" - " must be less than 8192 bytes"); + "Streaming parameters is not yet supported. " + "Parameter size must be less than 8192 bytes"); } - dataPtr = const_cast(static_cast(strParam->c_str())); + dataPtr = + const_cast( + static_cast( + strParam->c_str())); bufferLength = strParam->size() + 1 /* null terminator */; strLenOrIndPtr = AllocateParamBuffer(paramBuffers); *strLenOrIndPtr = SQL_NTS; break; } case SQL_C_WCHAR: { - if (!py::isinstance(param) && !py::isinstance(param) && + if (!py::isinstance(param) && + !py::isinstance(param) && !py::isinstance(param)) { - ThrowStdException(MakeParamMismatchErrorStr(paramInfo.paramCType, paramIndex)); + ThrowStdException( + MakeParamMismatchErrorStr( + paramInfo.paramCType, + paramIndex)); } if (paramInfo.isDAE) { // deferred execution LOG("Parameter[{}] is marked for DAE streaming", paramIndex); - dataPtr = const_cast(reinterpret_cast(¶mInfos[paramIndex])); + dataPtr = + const_cast( + reinterpret_cast( + ¶mInfos[paramIndex])); strLenOrIndPtr = AllocateParamBuffer(paramBuffers); *strLenOrIndPtr = SQL_LEN_DATA_AT_EXEC(0); bufferLength = 0; } else { // Normal small-string case - std::wstring* strParam = - AllocateParamBuffer(paramBuffers, param.cast()); - LOG("SQL_C_WCHAR Parameter[{}]: Length={}, isDAE={}", paramIndex, strParam->size(), paramInfo.isDAE); + std::wstring* strParam = AllocateParamBuffer( + paramBuffers, param.cast()); + LOG( + "SQL_C_WCHAR Parameter[{}]: Length={}, isDAE={}", + paramIndex, strParam->size(), + paramInfo.isDAE); std::vector* sqlwcharBuffer = - AllocateParamBuffer>(paramBuffers, WStringToSQLWCHAR(*strParam)); + AllocateParamBuffer>( + paramBuffers, WStringToSQLWCHAR(*strParam)); dataPtr = sqlwcharBuffer->data(); bufferLength = sqlwcharBuffer->size() * sizeof(SQLWCHAR); strLenOrIndPtr = AllocateParamBuffer(paramBuffers); @@ -295,15 +351,24 @@ SQLRETURN BindParameters(SQLHANDLE hStmt, const py::list& params, } case SQL_C_BIT: { if (!py::isinstance(param)) { - ThrowStdException(MakeParamMismatchErrorStr(paramInfo.paramCType, paramIndex)); + ThrowStdException( + MakeParamMismatchErrorStr( + paramInfo.paramCType, + paramIndex)); } dataPtr = - static_cast(AllocateParamBuffer(paramBuffers, param.cast())); + static_cast( + AllocateParamBuffer( + paramBuffers, + param.cast())); break; } case SQL_C_DEFAULT: { if (!py::isinstance(param)) { - ThrowStdException(MakeParamMismatchErrorStr(paramInfo.paramCType, paramIndex)); + ThrowStdException( + MakeParamMismatchErrorStr( + paramInfo.paramCType, + paramIndex)); } SQLSMALLINT sqlType = paramInfo.paramSQLType; SQLULEN columnSize = paramInfo.columnSize; @@ -319,10 +384,12 @@ SQLRETURN BindParameters(SQLHANDLE hStmt, const py::list& params, &describedType, &describedSize, &describedDigits, - &nullable - ); + &nullable); if (!SQL_SUCCEEDED(rc)) { - LOG("SQLDescribeParam failed for parameter {} with error code {}", paramIndex, rc); + LOG( + "SQLDescribeParam failed for parameter " + "{} with error code {}", + paramIndex, rc); return rc; } sqlType = describedType; @@ -343,131 +410,213 @@ SQLRETURN BindParameters(SQLHANDLE hStmt, const py::list& params, case SQL_C_SSHORT: case SQL_C_SHORT: { if (!py::isinstance(param)) { - ThrowStdException(MakeParamMismatchErrorStr(paramInfo.paramCType, paramIndex)); + ThrowStdException( + MakeParamMismatchErrorStr( + paramInfo.paramCType, + paramIndex)); } int value = param.cast(); // Range validation for signed 16-bit integer - if (value < std::numeric_limits::min() || value > std::numeric_limits::max()) { - ThrowStdException("Signed short integer parameter out of range at paramIndex " + std::to_string(paramIndex)); + if (value < std::numeric_limits::min() || + value > std::numeric_limits::max()) { + ThrowStdException( + "Signed short integer parameter out of " + "range at paramIndex " + + std::to_string(paramIndex)); } dataPtr = - static_cast(AllocateParamBuffer(paramBuffers, param.cast())); + static_cast( + AllocateParamBuffer( + paramBuffers, + param.cast())); break; } case SQL_C_UTINYINT: case SQL_C_USHORT: { if (!py::isinstance(param)) { - ThrowStdException(MakeParamMismatchErrorStr(paramInfo.paramCType, paramIndex)); + ThrowStdException( + MakeParamMismatchErrorStr( + paramInfo.paramCType, + paramIndex)); } unsigned int value = param.cast(); - if (value > std::numeric_limits::max()) { - ThrowStdException("Unsigned short integer parameter out of range at paramIndex " + std::to_string(paramIndex)); + if (value > std::numeric_limits::max()) { + ThrowStdException( + "Unsigned short integer parameter out" + " of range at paramIndex " + + std::to_string(paramIndex)); } dataPtr = static_cast( - AllocateParamBuffer(paramBuffers, param.cast())); + AllocateParamBuffer( + paramBuffers, + param.cast())); break; } case SQL_C_SBIGINT: case SQL_C_SLONG: case SQL_C_LONG: { if (!py::isinstance(param)) { - ThrowStdException(MakeParamMismatchErrorStr(paramInfo.paramCType, paramIndex)); + ThrowStdException( + MakeParamMismatchErrorStr( + paramInfo.paramCType, + paramIndex)); } int64_t value = param.cast(); // Range validation for signed 64-bit integer - if (value < std::numeric_limits::min() || value > std::numeric_limits::max()) { - ThrowStdException("Signed 64-bit integer parameter out of range at paramIndex " + std::to_string(paramIndex)); + if (value < std::numeric_limits::min() || + value > std::numeric_limits::max()) { + ThrowStdException( + "Signed 64-bit integer parameter out" + " of range at paramIndex " + + std::to_string(paramIndex)); } dataPtr = static_cast( - AllocateParamBuffer(paramBuffers, param.cast())); + AllocateParamBuffer( + paramBuffers, + param.cast())); break; } case SQL_C_UBIGINT: case SQL_C_ULONG: { if (!py::isinstance(param)) { - ThrowStdException(MakeParamMismatchErrorStr(paramInfo.paramCType, paramIndex)); + ThrowStdException( + MakeParamMismatchErrorStr( + paramInfo.paramCType, + paramIndex)); } uint64_t value = param.cast(); // Range validation for unsigned 64-bit integer if (value > std::numeric_limits::max()) { - ThrowStdException("Unsigned 64-bit integer parameter out of range at paramIndex " + std::to_string(paramIndex)); + ThrowStdException( + "Unsigned 64-bit integer parameter out" + " of range at paramIndex " + + std::to_string(paramIndex)); } dataPtr = static_cast( - AllocateParamBuffer(paramBuffers, param.cast())); + AllocateParamBuffer( + paramBuffers, + param.cast())); break; } case SQL_C_FLOAT: { if (!py::isinstance(param)) { - ThrowStdException(MakeParamMismatchErrorStr(paramInfo.paramCType, paramIndex)); + ThrowStdException( + MakeParamMismatchErrorStr( + paramInfo.paramCType, + paramIndex)); } dataPtr = static_cast( - AllocateParamBuffer(paramBuffers, param.cast())); + AllocateParamBuffer( + paramBuffers, + param.cast())); break; } case SQL_C_DOUBLE: { if (!py::isinstance(param)) { - ThrowStdException(MakeParamMismatchErrorStr(paramInfo.paramCType, paramIndex)); + ThrowStdException( + MakeParamMismatchErrorStr( + paramInfo.paramCType, + paramIndex)); } dataPtr = static_cast( - AllocateParamBuffer(paramBuffers, param.cast())); + AllocateParamBuffer( + paramBuffers, + param.cast())); break; } case SQL_C_TYPE_DATE: { py::object dateType = py::module_::import("datetime").attr("date"); if (!py::isinstance(param, dateType)) { - ThrowStdException(MakeParamMismatchErrorStr(paramInfo.paramCType, paramIndex)); + ThrowStdException( + MakeParamMismatchErrorStr( + paramInfo.paramCType, + paramIndex)); } int year = param.attr("year").cast(); if (year < 1753 || year > 9999) { - ThrowStdException("Date out of range for SQL Server (1753-9999) at paramIndex " + std::to_string(paramIndex)); - } - // TODO: can be moved to python by registering SQL_DATE_STRUCT in pybind - SQL_DATE_STRUCT* sqlDatePtr = AllocateParamBuffer(paramBuffers); - sqlDatePtr->year = static_cast(param.attr("year").cast()); - sqlDatePtr->month = static_cast(param.attr("month").cast()); - sqlDatePtr->day = static_cast(param.attr("day").cast()); + ThrowStdException( + "Date out of range for SQL Server " + "(1753-9999) at paramIndex " + + std::to_string(paramIndex)); + } + // TODO: can be moved to python by registering + // SQL_DATE_STRUCT in pybind + SQL_DATE_STRUCT* sqlDatePtr = + AllocateParamBuffer(paramBuffers); + sqlDatePtr->year = + static_cast(param.attr("year").cast()); + sqlDatePtr->month = + static_cast(param.attr("month").cast()); + sqlDatePtr->day = + static_cast(param.attr("day").cast()); dataPtr = static_cast(sqlDatePtr); break; } case SQL_C_TYPE_TIME: { - py::object timeType = py::module_::import("datetime").attr("time"); + py::object timeType = + py::module_::import("datetime").attr("time"); if (!py::isinstance(param, timeType)) { - ThrowStdException(MakeParamMismatchErrorStr(paramInfo.paramCType, paramIndex)); - } - // TODO: can be moved to python by registering SQL_TIME_STRUCT in pybind - SQL_TIME_STRUCT* sqlTimePtr = AllocateParamBuffer(paramBuffers); - sqlTimePtr->hour = static_cast(param.attr("hour").cast()); - sqlTimePtr->minute = static_cast(param.attr("minute").cast()); - sqlTimePtr->second = static_cast(param.attr("second").cast()); + ThrowStdException( + MakeParamMismatchErrorStr( + paramInfo.paramCType, + paramIndex)); + } + // TODO: can be moved to python by registering + // SQL_TIME_STRUCT in pybind + SQL_TIME_STRUCT* sqlTimePtr = + AllocateParamBuffer(paramBuffers); + sqlTimePtr->hour = + static_cast(param.attr("hour").cast()); + sqlTimePtr->minute = + static_cast(param.attr("minute").cast()); + sqlTimePtr->second = + static_cast(param.attr("second").cast()); dataPtr = static_cast(sqlTimePtr); break; } case SQL_C_TYPE_TIMESTAMP: { - py::object datetimeType = py::module_::import("datetime").attr("datetime"); + py::object datetimeType = + py::module_::import("datetime").attr("datetime"); if (!py::isinstance(param, datetimeType)) { - ThrowStdException(MakeParamMismatchErrorStr(paramInfo.paramCType, paramIndex)); + ThrowStdException( + MakeParamMismatchErrorStr( + paramInfo.paramCType, + paramIndex)); } SQL_TIMESTAMP_STRUCT* sqlTimestampPtr = AllocateParamBuffer(paramBuffers); - sqlTimestampPtr->year = static_cast(param.attr("year").cast()); - sqlTimestampPtr->month = static_cast(param.attr("month").cast()); - sqlTimestampPtr->day = static_cast(param.attr("day").cast()); - sqlTimestampPtr->hour = static_cast(param.attr("hour").cast()); - sqlTimestampPtr->minute = static_cast(param.attr("minute").cast()); - sqlTimestampPtr->second = static_cast(param.attr("second").cast()); + sqlTimestampPtr->year = + static_cast(param.attr("year").cast()); + sqlTimestampPtr->month = + static_cast(param.attr("month").cast()); + sqlTimestampPtr->day = + static_cast(param.attr("day").cast()); + sqlTimestampPtr->hour = + static_cast(param.attr("hour").cast()); + sqlTimestampPtr->minute = + static_cast(param.attr("minute").cast()); + sqlTimestampPtr->second = + static_cast(param.attr("second").cast()); // SQL server supports in ns, but python datetime supports in µs sqlTimestampPtr->fraction = static_cast( - param.attr("microsecond").cast() * 1000); // Convert µs to ns + // Convert µs to ns + param.attr("microsecond").cast() * 1000); dataPtr = static_cast(sqlTimestampPtr); break; } case SQL_C_NUMERIC: { if (!py::isinstance(param)) { - ThrowStdException(MakeParamMismatchErrorStr(paramInfo.paramCType, paramIndex)); + ThrowStdException( + MakeParamMismatchErrorStr( + paramInfo.paramCType, + paramIndex)); } NumericData decimalParam = param.cast(); - LOG("Received numeric parameter: precision - {}, scale- {}, sign - {}, value - {}", - decimalParam.precision, decimalParam.scale, decimalParam.sign, + LOG("Received numeric parameter: precision - " + "{}, scale- {}, sign - {}, value - {}", + decimalParam.precision, + decimalParam.scale, + decimalParam.sign, decimalParam.val); SQL_NUMERIC_STRUCT* decimalPtr = AllocateParamBuffer(paramBuffers); @@ -475,14 +624,18 @@ SQLRETURN BindParameters(SQLHANDLE hStmt, const py::list& params, decimalPtr->scale = decimalParam.scale; decimalPtr->sign = decimalParam.sign; // Convert the integer decimalParam.val to char array - std::memset(static_cast(decimalPtr->val), 0, sizeof(decimalPtr->val)); - std::memcpy(static_cast(decimalPtr->val), - reinterpret_cast(&decimalParam.val), - sizeof(decimalParam.val)); + std::memset( + static_cast(decimalPtr->val), + 0, + sizeof(decimalPtr->val)); + std::memcpy( + static_cast(decimalPtr->val), + reinterpret_cast(&decimalParam.val), + sizeof(decimalParam.val)); dataPtr = static_cast(decimalPtr); // TODO: Remove these lines - //strLenOrIndPtr = AllocateParamBuffer(paramBuffers); - //*strLenOrIndPtr = sizeof(SQL_NUMERIC_STRUCT); + // strLenOrIndPtr = AllocateParamBuffer(paramBuffers); + // *strLenOrIndPtr = sizeof(SQL_NUMERIC_STRUCT); break; } case SQL_C_GUID: { @@ -490,8 +643,8 @@ SQLRETURN BindParameters(SQLHANDLE hStmt, const py::list& params, } default: { std::ostringstream errorString; - errorString << "Unsupported parameter type - " << paramInfo.paramCType - << " for parameter - " << paramIndex; + errorString << "Unsupported parameter type - " << + paramInfo.paramCType << " for parameter - " << paramIndex; ThrowStdException(errorString.str()); } } @@ -502,58 +655,84 @@ SQLRETURN BindParameters(SQLHANDLE hStmt, const py::list& params, static_cast(paramIndex + 1), /* 1-based indexing */ static_cast(paramInfo.inputOutputType), static_cast(paramInfo.paramCType), - static_cast(paramInfo.paramSQLType), paramInfo.columnSize, - paramInfo.decimalDigits, dataPtr, bufferLength, strLenOrIndPtr); + static_cast(paramInfo.paramSQLType), + paramInfo.columnSize, + paramInfo.decimalDigits, + dataPtr, + bufferLength, + strLenOrIndPtr); if (!SQL_SUCCEEDED(rc)) { LOG("Error when binding parameter - {}", paramIndex); return rc; } - // Special handling for Numeric type - - // https://learn.microsoft.com/en-us/sql/odbc/reference/appendixes/retrieve-numeric-data-sql-numeric-struct-kb222831?view=sql-server-ver16#sql_c_numeric-overview - if (paramInfo.paramCType == SQL_C_NUMERIC) { - SQLHDESC hDesc = nullptr; - rc = SQLGetStmtAttr_ptr(hStmt, SQL_ATTR_APP_PARAM_DESC, &hDesc, 0, NULL); - if(!SQL_SUCCEEDED(rc)) { - LOG("Error when getting statement attribute - {}", paramIndex); - return rc; - } - rc = SQLSetDescField_ptr(hDesc, 1, SQL_DESC_TYPE, (SQLPOINTER) SQL_C_NUMERIC, 0); - if(!SQL_SUCCEEDED(rc)) { - LOG("Error when setting descriptor field SQL_DESC_TYPE - {}", paramIndex); - return rc; - } - SQL_NUMERIC_STRUCT* numericPtr = reinterpret_cast(dataPtr); - rc = SQLSetDescField_ptr(hDesc, 1, SQL_DESC_PRECISION, - (SQLPOINTER) numericPtr->precision, 0); - if(!SQL_SUCCEEDED(rc)) { - LOG("Error when setting descriptor field SQL_DESC_PRECISION - {}", paramIndex); - return rc; - } - - rc = SQLSetDescField_ptr(hDesc, 1, SQL_DESC_SCALE, - (SQLPOINTER) numericPtr->scale, 0); - if(!SQL_SUCCEEDED(rc)) { - LOG("Error when setting descriptor field SQL_DESC_SCALE - {}", paramIndex); - return rc; - } - - rc = SQLSetDescField_ptr(hDesc, 1, SQL_DESC_DATA_PTR, (SQLPOINTER) numericPtr, 0); - if(!SQL_SUCCEEDED(rc)) { - LOG("Error when setting descriptor field SQL_DESC_DATA_PTR - {}", paramIndex); - return rc; + // Special handling for Numeric type - + // https://learn.microsoft.com/en-us/sql/odbc/reference/appendixes/retrieve-numeric-data-sql-numeric-struct-kb222831?view=sql-server-ver16#sql_c_numeric-overview + if (paramInfo.paramCType == SQL_C_NUMERIC) { + SQLHDESC hDesc = nullptr; + rc = SQLGetStmtAttr_ptr( + hStmt, + SQL_ATTR_APP_PARAM_DESC, + &hDesc, 0, NULL); + if (!SQL_SUCCEEDED(rc)) { + LOG( + "Error when getting statement attribute - {}", + paramIndex); + return rc; + } + rc = SQLSetDescField_ptr( + hDesc, 1, SQL_DESC_TYPE, + (SQLPOINTER) SQL_C_NUMERIC, 0); + if (!SQL_SUCCEEDED(rc)) { + LOG( + "Error when setting descriptor field SQL_DESC_TYPE - {}", + paramIndex); + return rc; + } + SQL_NUMERIC_STRUCT* numericPtr = + reinterpret_cast(dataPtr); + rc = SQLSetDescField_ptr( + hDesc, 1, SQL_DESC_PRECISION, + (SQLPOINTER) numericPtr->precision, 0); + if (!SQL_SUCCEEDED(rc)) { + LOG( + "Error when setting descriptor field " + "SQL_DESC_PRECISION - {}", paramIndex); + return rc; + } + rc = SQLSetDescField_ptr( + hDesc, 1, SQL_DESC_SCALE, + (SQLPOINTER) numericPtr->scale, 0); + if (!SQL_SUCCEEDED(rc)) { + LOG( + "Error when setting descriptor field " + "SQL_DESC_SCALE - {}", paramIndex); + return rc; + } + rc = SQLSetDescField_ptr( + hDesc, 1, SQL_DESC_DATA_PTR, (SQLPOINTER) numericPtr, 0); + if (!SQL_SUCCEEDED(rc)) { + LOG( + "Error when setting descriptor field " + "SQL_DESC_DATA_PTR - {}", paramIndex); + return rc; + } } - } } - LOG("Finished parameter binding. Number of parameters: {}", params.size()); + LOG( + "Finished parameter binding. Number of parameters: {}", + params.size()); return SQL_SUCCESS; } -// This is temporary hack to avoid crash when SQLDescribeCol returns 0 as columnSize -// for NVARCHAR(MAX) & similar types. Variable length data needs more nuanced handling. +// This is temporary hack to avoid crash when +// SQLDescribeCol returns 0 as columnSize +// for NVARCHAR(MAX) & similar types. +// Variable length data needs more nuanced handling. // TODO: Fix this in beta -// This function sets the buffer allocated to fetch NVARCHAR(MAX) & similar types to -// 4096 chars. So we'll retrieve data upto 4096. Anything greater then that will throw -// error +// This function sets the buffer allocated +// to fetch NVARCHAR(MAX) & similar types to +// 4096 chars. So we'll retrieve data upto 4096. +// Anything greater then that will throw error void HandleZeroColumnSizeAtFetch(SQLULEN& columnSize) { if (columnSize == 0) { columnSize = 4096; @@ -567,24 +746,32 @@ template void LOG(const std::string& formatString, Args&&... args) { py::gil_scoped_acquire gil; // <---- this ensures safe Python API usage - py::object logger = py::module_::import("mssql_python.logging_config").attr("get_logger")(); + py::object logger = + py::module_::import("mssql_python.logging_config") + .attr("get_logger")(); if (py::isinstance(logger)) return; try { - std::string ddbcFormatString = "[DDBC Bindings log] " + formatString; + std::string ddbcFormatString = + "[DDBC Bindings log] " + formatString; if constexpr (sizeof...(args) == 0) { logger.attr("debug")(py::str(ddbcFormatString)); } else { - py::str message = py::str(ddbcFormatString).format(std::forward(args)...); + py::str message = + py::str(ddbcFormatString) + .format(std::forward(args)...); logger.attr("debug")(message); } } catch (const std::exception& e) { - std::cerr << "Logging error: " << e.what() << std::endl; + std::cerr << "Logging error: " << + e.what() << std::endl; } } // TODO: Add more nuanced exception classes -void ThrowStdException(const std::string& message) { throw std::runtime_error(message); } +void ThrowStdException(const std::string& message) { + throw std::runtime_error(message); +} std::string GetLastErrorMessage(); // TODO: Move this to Python @@ -592,11 +779,13 @@ std::string GetModuleDirectory() { py::object module = py::module::import("mssql_python"); py::object module_path = module.attr("__file__"); std::string module_file = module_path.cast(); - + #ifdef _WIN32 // Windows-specific path handling char path[MAX_PATH]; - errno_t err = strncpy_s(path, MAX_PATH, module_file.c_str(), module_file.length()); + errno_t err = strncpy_s( + path, MAX_PATH, + module_file.c_str(), module_file.length()); if (err != 0) { LOG("strncpy_s failed with error code: {}", err); return {}; @@ -618,14 +807,17 @@ std::string GetModuleDirectory() { // Platform-agnostic function to load the driver dynamic library DriverHandle LoadDriverLibrary(const std::string& driverPath) { LOG("Loading driver from path: {}", driverPath); - + #ifdef _WIN32 // Windows: Convert string to wide string for LoadLibraryW std::wstring widePath(driverPath.begin(), driverPath.end()); HMODULE handle = LoadLibraryW(widePath.c_str()); if (!handle) { - LOG("Failed to load library: {}. Error: {}", driverPath, GetLastErrorMessage()); - ThrowStdException("Failed to load library: " + driverPath); + LOG( + "Failed to load library: {}. Error: {}", + driverPath, GetLastErrorMessage()); + ThrowStdException( + "Failed to load library: " + driverPath); } return handle; #else @@ -645,15 +837,20 @@ std::string GetLastErrorMessage() { DWORD error = GetLastError(); char* messageBuffer = nullptr; size_t size = FormatMessageA( - FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + FORMAT_MESSAGE_ALLOCATE_BUFFER | + FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, NULL, error, - MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + MAKELANGID(LANG_NEUTRAL, + SUBLANG_DEFAULT), (LPSTR)&messageBuffer, 0, - NULL - ); - std::string errorMessage = messageBuffer ? std::string(messageBuffer, size) : "Unknown error"; + NULL); + std::string errorMessage = + messageBuffer ? + std::string(messageBuffer, size) : + "Unknown error"; LocalFree(messageBuffer); return "Error code: " + std::to_string(error) + " - " + errorMessage; #else @@ -699,27 +896,38 @@ std::string GetDriverPathCpp(const std::string& moduleDir) { #ifdef __linux__ if (fs::exists("/etc/alpine-release")) { platform = "alpine"; - } else if (fs::exists("/etc/redhat-release") || fs::exists("/etc/centos-release")) { + } else if ( + fs::exists("/etc/redhat-release") || + fs::exists("/etc/centos-release")) { platform = "rhel"; - } else if (fs::exists("/etc/SuSE-release") || fs::exists("/etc/SUSE-brand")) { + } else if ( + fs::exists("/etc/SuSE-release") || + fs::exists("/etc/SUSE-brand")) { platform = "suse"; } else { - platform = "debian_ubuntu"; // Default to debian_ubuntu for other distros + // Default to debian_ubuntu for other distros + platform = "debian_ubuntu"; } - fs::path driverPath = basePath / "libs" / "linux" / platform / arch / "lib" / "libmsodbcsql-18.5.so.1.1"; + fs::path driverPath = + basePath / "libs" / "linux" / + platform / arch / "lib" / "libmsodbcsql-18.5.so.1.1"; return driverPath.string(); #elif defined(__APPLE__) platform = "macos"; - fs::path driverPath = basePath / "libs" / platform / arch / "lib" / "libmsodbcsql.18.dylib"; + fs::path driverPath = + basePath / "libs" / platform / + arch / "lib" / "libmsodbcsql.18.dylib"; return driverPath.string(); #elif defined(_WIN32) platform = "windows"; // Normalize x86_64 to x64 for Windows naming if (arch == "x86_64") arch = "x64"; - fs::path driverPath = basePath / "libs" / platform / arch / "msodbcsql18.dll"; + fs::path driverPath = + basePath / "libs" / platform / + arch / "msodbcsql18.dll"; return driverPath.string(); #else @@ -737,85 +945,133 @@ DriverHandle LoadDriverOrThrowException() { LOG("Architecture: {}", archStr); // Use only C++ function for driver path resolution - // Not using Python function since it causes circular import issues on Alpine Linux + // Not using Python function since it + // causes circular import issues on Alpine Linux // and other platforms with strict module loading rules. std::string driverPathStr = GetDriverPathCpp(moduleDir); - + fs::path driverPath(driverPathStr); - + LOG("Driver path determined: {}", driverPath.string()); #ifdef _WIN32 // On Windows, optionally load mssql-auth.dll if it exists std::string archDir = - (archStr == "win64" || archStr == "amd64" || archStr == "x64") ? "x64" : + (archStr == "win64" || archStr == "amd64" || + archStr == "x64") ? "x64" : (archStr == "arm64") ? "arm64" : "x86"; - + fs::path dllDir = fs::path(moduleDir) / "libs" / "windows" / archDir; fs::path authDllPath = dllDir / "mssql-auth.dll"; if (fs::exists(authDllPath)) { - HMODULE hAuth = LoadLibraryW(std::wstring(authDllPath.native().begin(), authDllPath.native().end()).c_str()); + std::wstring authDllWidePath( + authDllPath.native().begin(), + authDllPath.native().end()); + HMODULE hAuth = LoadLibraryW(authDllWidePath.c_str()); if (hAuth) { - LOG("mssql-auth.dll loaded: {}", authDllPath.string()); + LOG( + "mssql-auth.dll loaded: {}", + authDllPath.string()); } else { - LOG("Failed to load mssql-auth.dll: {}", GetLastErrorMessage()); - ThrowStdException("Failed to load mssql-auth.dll. Please ensure it is present in the expected directory."); + LOG( + "Failed to load mssql-auth.dll: {}", + GetLastErrorMessage()); + ThrowStdException( + "Failed to load mssql-auth.dll. " + "Please ensure it is present in the expected directory."); } } else { - LOG("Note: mssql-auth.dll not found. This is OK if Entra ID is not in use."); - ThrowStdException("mssql-auth.dll not found. If you are using Entra ID, please ensure it is present."); + LOG("Note: mssql-auth.dll not found. " + "This is OK if Entra ID is not in use."); + ThrowStdException( + "mssql-auth.dll not found. " + "If you are using Entra ID, please ensure it is present."); } #endif if (!fs::exists(driverPath)) { - ThrowStdException("ODBC driver not found at: " + driverPath.string()); + ThrowStdException( + "ODBC driver not found at: " + driverPath.string()); } DriverHandle handle = LoadDriverLibrary(driverPath.string()); if (!handle) { LOG("Failed to load driver: {}", GetLastErrorMessage()); - ThrowStdException("Failed to load the driver. Please read the documentation (https://github.com/microsoft/mssql-python#installation) to install the required dependencies."); + ThrowStdException( + "Failed to load the driver. Please read the documentation " + "(https://github.com/microsoft/mssql-python#installation) " + "to install the required dependencies."); } LOG("Driver library successfully loaded."); // Load function pointers using helper - SQLAllocHandle_ptr = GetFunctionPointer(handle, "SQLAllocHandle"); - SQLSetEnvAttr_ptr = GetFunctionPointer(handle, "SQLSetEnvAttr"); - SQLSetConnectAttr_ptr = GetFunctionPointer(handle, "SQLSetConnectAttrW"); - SQLSetStmtAttr_ptr = GetFunctionPointer(handle, "SQLSetStmtAttrW"); - SQLGetConnectAttr_ptr = GetFunctionPointer(handle, "SQLGetConnectAttrW"); - - SQLDriverConnect_ptr = GetFunctionPointer(handle, "SQLDriverConnectW"); - SQLExecDirect_ptr = GetFunctionPointer(handle, "SQLExecDirectW"); - SQLPrepare_ptr = GetFunctionPointer(handle, "SQLPrepareW"); - SQLBindParameter_ptr = GetFunctionPointer(handle, "SQLBindParameter"); - SQLExecute_ptr = GetFunctionPointer(handle, "SQLExecute"); - SQLRowCount_ptr = GetFunctionPointer(handle, "SQLRowCount"); - SQLGetStmtAttr_ptr = GetFunctionPointer(handle, "SQLGetStmtAttrW"); - SQLSetDescField_ptr = GetFunctionPointer(handle, "SQLSetDescFieldW"); - - SQLFetch_ptr = GetFunctionPointer(handle, "SQLFetch"); - SQLFetchScroll_ptr = GetFunctionPointer(handle, "SQLFetchScroll"); - SQLGetData_ptr = GetFunctionPointer(handle, "SQLGetData"); - SQLNumResultCols_ptr = GetFunctionPointer(handle, "SQLNumResultCols"); - SQLBindCol_ptr = GetFunctionPointer(handle, "SQLBindCol"); - SQLDescribeCol_ptr = GetFunctionPointer(handle, "SQLDescribeColW"); - SQLMoreResults_ptr = GetFunctionPointer(handle, "SQLMoreResults"); - SQLColAttribute_ptr = GetFunctionPointer(handle, "SQLColAttributeW"); - - SQLEndTran_ptr = GetFunctionPointer(handle, "SQLEndTran"); - SQLDisconnect_ptr = GetFunctionPointer(handle, "SQLDisconnect"); - SQLFreeHandle_ptr = GetFunctionPointer(handle, "SQLFreeHandle"); - SQLFreeStmt_ptr = GetFunctionPointer(handle, "SQLFreeStmt"); - - SQLGetDiagRec_ptr = GetFunctionPointer(handle, "SQLGetDiagRecW"); - - SQLParamData_ptr = GetFunctionPointer(handle, "SQLParamData"); - SQLPutData_ptr = GetFunctionPointer(handle, "SQLPutData"); - SQLTables_ptr = GetFunctionPointer(handle, "SQLTablesW"); - - SQLDescribeParam_ptr = GetFunctionPointer(handle, "SQLDescribeParam"); + SQLAllocHandle_ptr = GetFunctionPointer( + handle, "SQLAllocHandle"); + SQLSetEnvAttr_ptr = GetFunctionPointer( + handle, "SQLSetEnvAttr"); + SQLSetConnectAttr_ptr = GetFunctionPointer( + handle, "SQLSetConnectAttrW"); + SQLSetStmtAttr_ptr = GetFunctionPointer( + handle, "SQLSetStmtAttrW"); + SQLGetConnectAttr_ptr = GetFunctionPointer( + handle, "SQLGetConnectAttrW"); + + SQLDriverConnect_ptr = GetFunctionPointer( + handle, "SQLDriverConnectW"); + SQLExecDirect_ptr = GetFunctionPointer( + handle, "SQLExecDirectW"); + SQLPrepare_ptr = GetFunctionPointer( + handle, "SQLPrepareW"); + SQLBindParameter_ptr = GetFunctionPointer( + handle, "SQLBindParameter"); + SQLExecute_ptr = GetFunctionPointer( + handle, "SQLExecute"); + SQLRowCount_ptr = GetFunctionPointer( + handle, "SQLRowCount"); + SQLGetStmtAttr_ptr = GetFunctionPointer( + handle, "SQLGetStmtAttrW"); + SQLSetDescField_ptr = GetFunctionPointer( + handle, "SQLSetDescFieldW"); + + SQLFetch_ptr = GetFunctionPointer( + handle, "SQLFetch"); + SQLFetchScroll_ptr = GetFunctionPointer( + handle, "SQLFetchScroll"); + SQLGetData_ptr = GetFunctionPointer( + handle, "SQLGetData"); + SQLNumResultCols_ptr = GetFunctionPointer( + handle, "SQLNumResultCols"); + SQLBindCol_ptr = GetFunctionPointer( + handle, "SQLBindCol"); + SQLDescribeCol_ptr = GetFunctionPointer( + handle, "SQLDescribeColW"); + SQLMoreResults_ptr = GetFunctionPointer( + handle, "SQLMoreResults"); + SQLColAttribute_ptr = GetFunctionPointer( + handle, "SQLColAttributeW"); + + SQLEndTran_ptr = GetFunctionPointer( + handle, "SQLEndTran"); + SQLDisconnect_ptr = GetFunctionPointer( + handle, "SQLDisconnect"); + SQLFreeHandle_ptr = GetFunctionPointer( + handle, "SQLFreeHandle"); + SQLFreeStmt_ptr = GetFunctionPointer( + handle, "SQLFreeStmt"); + + SQLGetDiagRec_ptr = GetFunctionPointer( + handle, "SQLGetDiagRecW"); + + SQLParamData_ptr = GetFunctionPointer( + handle, "SQLParamData"); + SQLPutData_ptr = GetFunctionPointer( + handle, "SQLPutData"); + SQLTables_ptr = GetFunctionPointer( + handle, "SQLTablesW"); + + SQLDescribeParam_ptr = GetFunctionPointer( + handle, "SQLDescribeParam"); bool success = SQLAllocHandle_ptr && SQLSetEnvAttr_ptr && SQLSetConnectAttr_ptr && @@ -831,13 +1087,14 @@ DriverHandle LoadDriverOrThrowException() { SQLDescribeParam_ptr; if (!success) { - ThrowStdException("Failed to load required function pointers from driver."); + ThrowStdException( + "Failed to load required function pointers from driver."); } LOG("All driver function pointers successfully loaded."); return handle; } -// DriverLoader definition +// DriverLoader definition DriverLoader::DriverLoader() : m_driverLoaded(false) {} DriverLoader& DriverLoader::getInstance() { @@ -889,17 +1146,19 @@ void SqlHandle::free() { } SQLFreeHandle_ptr(_type, _handle); _handle = nullptr; - // Don't log during destruction - it can cause segfaults during Python shutdown + // Don't log during destruction - + // it can cause segfaults during Python shutdown } } // Helper function to check for driver errors -ErrorInfo SQLCheckError_Wrap(SQLSMALLINT handleType, SqlHandlePtr handle, SQLRETURN retcode) { - LOG("Checking errors for retcode - {}" , retcode); +ErrorInfo SQLCheckError_Wrap( + SQLSMALLINT handleType, SqlHandlePtr handle, SQLRETURN retcode) { + LOG("Checking errors for retcode - {}", retcode); ErrorInfo errorInfo; if (retcode == SQL_INVALID_HANDLE) { LOG("Invalid handle received"); - errorInfo.ddbcErrorMsg = std::wstring( L"Invalid handle!"); + errorInfo.ddbcErrorMsg = std::wstring(L"Invalid handle!"); return errorInfo; } assert(handle != 0); @@ -915,8 +1174,10 @@ ErrorInfo SQLCheckError_Wrap(SQLSMALLINT handleType, SqlHandlePtr handle, SQLRET SQLSMALLINT messageLen; SQLRETURN diagReturn = - SQLGetDiagRec_ptr(handleType, rawHandle, 1, sqlState, - &nativeError, message, SQL_MAX_MESSAGE_LENGTH, &messageLen); + SQLGetDiagRec_ptr( + handleType, rawHandle, 1, sqlState, + &nativeError, message, + SQL_MAX_MESSAGE_LENGTH, &messageLen); if (SQL_SUCCEEDED(diagReturn)) { #if defined(_WIN32) @@ -924,7 +1185,8 @@ ErrorInfo SQLCheckError_Wrap(SQLSMALLINT handleType, SqlHandlePtr handle, SQLRET errorInfo.sqlState = std::wstring(sqlState); errorInfo.ddbcErrorMsg = std::wstring(message); #else - // On macOS/Linux, need to convert SQLWCHAR (usually unsigned short) to wchar_t + // On macOS/Linux, need to convert SQLWCHAR + // (usually unsigned short) to wchar_t errorInfo.sqlState = SQLWCHARToWString(sqlState); errorInfo.ddbcErrorMsg = SQLWCHARToWString(message, messageLen); #endif @@ -939,67 +1201,76 @@ py::list SQLGetAllDiagRecords(SqlHandlePtr handle) { LOG("Function pointer not initialized. Loading the driver."); DriverLoader::getInstance().loadDriver(); } - + py::list records; SQLHANDLE rawHandle = handle->get(); SQLSMALLINT handleType = handle->type(); - + // Iterate through all available diagnostic records for (SQLSMALLINT recNumber = 1; ; recNumber++) { SQLWCHAR sqlState[6] = {0}; SQLWCHAR message[SQL_MAX_MESSAGE_LENGTH] = {0}; SQLINTEGER nativeError = 0; SQLSMALLINT messageLen = 0; - + SQLRETURN diagReturn = SQLGetDiagRec_ptr( - handleType, rawHandle, recNumber, sqlState, &nativeError, + handleType, rawHandle, recNumber, sqlState, &nativeError, message, SQL_MAX_MESSAGE_LENGTH, &messageLen); - + if (diagReturn == SQL_NO_DATA || !SQL_SUCCEEDED(diagReturn)) break; - + #if defined(_WIN32) // On Windows, create a formatted UTF-8 string for state+error - // Convert SQLWCHAR sqlState to UTF-8 - int stateSize = WideCharToMultiByte(CP_UTF8, 0, sqlState, -1, NULL, 0, NULL, NULL); + int stateSize = WideCharToMultiByte( + CP_UTF8, 0, sqlState, -1, NULL, 0, NULL, NULL); std::vector stateBuffer(stateSize); - WideCharToMultiByte(CP_UTF8, 0, sqlState, -1, stateBuffer.data(), stateSize, NULL, NULL); - + WideCharToMultiByte( + CP_UTF8, 0, sqlState, -1, stateBuffer.data(), + stateSize, NULL, NULL); + // Format the state with error code - std::string stateWithError = "[" + std::string(stateBuffer.data()) + "] (" + std::to_string(nativeError) + ")"; - + std::string stateWithError = + "[" + std::string(stateBuffer.data()) + + "] (" + std::to_string(nativeError) + ")"; + // Convert wide string message to UTF-8 - int msgSize = WideCharToMultiByte(CP_UTF8, 0, message, -1, NULL, 0, NULL, NULL); + int msgSize = WideCharToMultiByte( + CP_UTF8, 0, message, -1, NULL, 0, NULL, NULL); std::vector msgBuffer(msgSize); - WideCharToMultiByte(CP_UTF8, 0, message, -1, msgBuffer.data(), msgSize, NULL, NULL); - + WideCharToMultiByte( + CP_UTF8, 0, message, -1, msgBuffer.data(), + msgSize, NULL, NULL); + // Create the tuple with converted strings records.append(py::make_tuple( py::str(stateWithError), - py::str(msgBuffer.data()) - )); + py::str(msgBuffer.data()))); #else - // On Unix, use the SQLWCHARToWString utility and then convert to UTF-8 + // On Unix, use the SQLWCHARToWString + // utility and then convert to UTF-8 std::string stateStr = WideToUTF8(SQLWCHARToWString(sqlState)); - std::string msgStr = WideToUTF8(SQLWCHARToWString(message, messageLen)); - + std::string msgStr = WideToUTF8( + SQLWCHARToWString(message, messageLen)); + // Format the state string - std::string stateWithError = "[" + stateStr + "] (" + std::to_string(nativeError) + ")"; - + std::string stateWithError = + "[" + stateStr + "] (" + + std::to_string(nativeError) + ")"; + // Create the tuple with converted strings records.append(py::make_tuple( py::str(stateWithError), - py::str(msgStr) - )); + py::str(msgStr))); #endif } - return records; } // Wrap SQLExecDirect -SQLRETURN SQLExecDirect_wrap(SqlHandlePtr StatementHandle, const std::wstring& Query) { +SQLRETURN SQLExecDirect_wrap( + SqlHandlePtr StatementHandle, const std::wstring& Query) { LOG("Execute SQL query directly - {}", Query.c_str()); if (!SQLExecDirect_ptr) { LOG("Function pointer not initialized. Loading the driver."); @@ -1025,7 +1296,8 @@ SQLRETURN SQLExecDirect_wrap(SqlHandlePtr StatementHandle, const std::wstring& Q #else queryPtr = const_cast(Query.c_str()); #endif - SQLRETURN ret = SQLExecDirect_ptr(StatementHandle->get(), queryPtr, SQL_NTS); + SQLRETURN ret = SQLExecDirect_ptr( + StatementHandle->get(), queryPtr, SQL_NTS); if (!SQL_SUCCEEDED(ret)) { LOG("Failed to execute query directly"); } @@ -1033,12 +1305,12 @@ SQLRETURN SQLExecDirect_wrap(SqlHandlePtr StatementHandle, const std::wstring& Q } // Wrapper for SQLTables -SQLRETURN SQLTables_wrap(SqlHandlePtr StatementHandle, +SQLRETURN SQLTables_wrap(SqlHandlePtr StatementHandle, const std::wstring& catalog, - const std::wstring& schema, + const std::wstring& schema, const std::wstring& table, const std::wstring& tableType) { - + if (!SQLTables_ptr) { LOG("Function pointer not initialized. Loading the driver."); DriverLoader::getInstance().loadDriver(); @@ -1105,8 +1377,7 @@ SQLRETURN SQLTables_wrap(SqlHandlePtr StatementHandle, catalogPtr, catalogLen, schemaPtr, schemaLen, tablePtr, tableLen, - tableTypePtr, tableTypeLen - ); + tableTypePtr, tableTypeLen); if (!SQL_SUCCEEDED(ret)) { LOG("SQLTables failed with return code: {}", ret); @@ -1117,23 +1388,34 @@ SQLRETURN SQLTables_wrap(SqlHandlePtr StatementHandle, return ret; } -// Executes the provided query. If the query is parametrized, it prepares the statement and -// binds the parameters. Otherwise, it executes the query directly. -// 'usePrepare' parameter can be used to disable the prepare step for queries that might already +// Executes the provided query. If the query is parametrized, +// it prepares the statement and binds the parameters. +// Otherwise, it executes the query directly. +// 'usePrepare' parameter can be used to disable +// the prepare step for queries that might already // be prepared in a previous call. SQLRETURN SQLExecute_wrap(const SqlHandlePtr statementHandle, const std::wstring& query /* TODO: Use SQLTCHAR? */, - const py::list& params, std::vector& paramInfos, - py::list& isStmtPrepared, const bool usePrepare = true) { + const py::list& params, + std::vector& paramInfos, + py::list& isStmtPrepared, + const bool usePrepare = true) { LOG("Execute SQL Query - {}", query.c_str()); if (!SQLPrepare_ptr) { - LOG("Function pointer not initialized. Loading the driver."); - DriverLoader::getInstance().loadDriver(); // Load the driver + LOG("Function pointer not initialized. " + "Loading the driver."); + // Load the driver + DriverLoader::getInstance().loadDriver(); } - assert(SQLPrepare_ptr && SQLBindParameter_ptr && SQLExecute_ptr && SQLExecDirect_ptr); + assert( + SQLPrepare_ptr && + SQLBindParameter_ptr && + SQLExecute_ptr && + SQLExecDirect_ptr); if (params.size() != paramInfos.size()) { - // TODO: This should be a special internal exception, that python wont relay to users as is + // TODO: This should be a special internal exception, + // that python wont relay to users as is ThrowStdException("Number of parameters and paramInfos do not match"); } @@ -1163,8 +1445,9 @@ SQLRETURN SQLExecute_wrap(const SqlHandlePtr statementHandle, queryPtr = const_cast(query.c_str()); #endif if (params.size() == 0) { - // Execute statement directly if the statement is not parametrized. This is the - // fastest way to submit a SQL statement for one-time execution according to + // Execute statement directly if the statement is not parametrized. + // This is the fastest way to submit a SQL statement + // for one-time execution according to // DDBC documentation - // https://learn.microsoft.com/en-us/sql/odbc/reference/syntax/sqlexecdirect-function?view=sql-server-ver16 rc = SQLExecDirect_ptr(hStmt, queryPtr, SQL_NTS); @@ -1173,8 +1456,10 @@ SQLRETURN SQLExecute_wrap(const SqlHandlePtr statementHandle, } return rc; } else { - // isStmtPrepared is a list instead of a bool coz bools in Python are immutable. - // Hence, we can't pass around bools by reference & modify them. Therefore, isStmtPrepared + // isStmtPrepared is a list instead of a bool coz bools in + // Python are immutable. + // Hence, we can't pass around bools by reference & modify them. + // Therefore, isStmtPrepared // must be a list with exactly one bool element assert(isStmtPrepared.size() == 1); if (usePrepare) { @@ -1185,7 +1470,8 @@ SQLRETURN SQLExecute_wrap(const SqlHandlePtr statementHandle, } isStmtPrepared[0] = py::cast(true); } else { - // Make sure the statement has been prepared earlier if we're not preparing now + // Make sure the statement has been prepared + // earlier if we're not preparing now bool isStmtPreparedAsBool = isStmtPrepared[0].cast(); if (!isStmtPreparedAsBool) { // TODO: Print the query @@ -1193,7 +1479,8 @@ SQLRETURN SQLExecute_wrap(const SqlHandlePtr statementHandle, } } - // This vector manages the heap memory allocated for parameter buffers. + // This vector manages the heap memory + // allocated for parameter buffers. // It must be in scope until SQLExecute is done. std::vector> paramBuffers; rc = BindParameters(hStmt, params, paramInfos, paramBuffers); @@ -1204,18 +1491,21 @@ SQLRETURN SQLExecute_wrap(const SqlHandlePtr statementHandle, rc = SQLExecute_ptr(hStmt); if (rc == SQL_NEED_DATA) { LOG("Beginning SQLParamData/SQLPutData loop for DAE."); - SQLPOINTER paramToken = nullptr; + SQLPOINTER paramToken = nullptr; while ((rc = SQLParamData_ptr(hStmt, ¶mToken)) == SQL_NEED_DATA) { // Finding the paramInfo that matches the returned token const ParamInfo* matchedInfo = nullptr; for (auto& info : paramInfos) { - if (reinterpret_cast(const_cast(&info)) == paramToken) { + if ( + reinterpret_cast( + const_cast(&info)) == paramToken) { matchedInfo = &info; break; } } if (!matchedInfo) { - ThrowStdException("Unrecognized paramToken returned by SQLParamData"); + ThrowStdException( + "Unrecognized paramToken returned by SQLParamData"); } const py::object& pyObj = matchedInfo->dataPtr; if (pyObj.is_none()) { @@ -1238,14 +1528,25 @@ SQLRETURN SQLExecute_wrap(const SqlHandlePtr statementHandle, size_t offset = 0; size_t chunkChars = DAE_CHUNK_SIZE / sizeof(SQLWCHAR); while (offset < totalChars) { - size_t len = std::min(chunkChars, totalChars - offset); - size_t lenBytes = len * sizeof(SQLWCHAR); - if (lenBytes > static_cast(std::numeric_limits::max())) { - ThrowStdException("Chunk size exceeds maximum allowed by SQLLEN"); + size_t len = + std::min(chunkChars, totalChars - offset); + size_t lenBytes = + len * sizeof(SQLWCHAR); + if ( + lenBytes > + static_cast( + std::numeric_limits::max())) { + ThrowStdException( + "Chunk size exceeds maximum allowed by SQLLEN"); } - rc = SQLPutData_ptr(hStmt, (SQLPOINTER)(dataPtr + offset), static_cast(lenBytes)); + rc = SQLPutData_ptr( + hStmt, + (SQLPOINTER)(dataPtr + offset), + static_cast(lenBytes)); if (!SQL_SUCCEEDED(rc)) { - LOG("SQLPutData failed at offset {} of {}", offset, totalChars); + LOG( + "SQLPutData failed at offset {} of {}", + offset, totalChars); return rc; } offset += len; @@ -1257,11 +1558,17 @@ SQLRETURN SQLExecute_wrap(const SqlHandlePtr statementHandle, size_t offset = 0; size_t chunkBytes = DAE_CHUNK_SIZE; while (offset < totalBytes) { - size_t len = std::min(chunkBytes, totalBytes - offset); + size_t len = + std::min(chunkBytes, totalBytes - offset); - rc = SQLPutData_ptr(hStmt, (SQLPOINTER)(dataPtr + offset), static_cast(len)); + rc = SQLPutData_ptr( + hStmt, + (SQLPOINTER)(dataPtr + offset), + static_cast(len)); if (!SQL_SUCCEEDED(rc)) { - LOG("SQLPutData failed at offset {} of {}", offset, totalBytes); + LOG( + "SQLPutData failed at offset {} of {}", + offset, totalBytes); return rc; } offset += len; @@ -1280,12 +1587,15 @@ SQLRETURN SQLExecute_wrap(const SqlHandlePtr statementHandle, LOG("DAE complete, SQLExecute resumed internally."); } if (!SQL_SUCCEEDED(rc) && rc != SQL_NO_DATA) { - LOG("DDBCSQLExecute: Error during execution of the statement"); + LOG("DDBCSQLExecute: " + "Error during execution of the statement"); return rc; } - // Unbind the bound buffers for all parameters coz the buffers' memory will - // be freed when this function exits (parambuffers goes out of scope) + // Unbind the bound buffers for + // all parameters coz the buffers' memory will + // be freed when this function exits + // (parambuffers goes out of scope) rc = SQLFreeStmt_ptr(hStmt, SQL_RESET_PARAMS); return rc; } @@ -1296,27 +1606,41 @@ SQLRETURN BindParameterArray(SQLHANDLE hStmt, const std::vector& paramInfos, size_t paramSetSize, std::vector>& paramBuffers) { - LOG("Starting column-wise parameter array binding. paramSetSize: {}, paramCount: {}", paramSetSize, columnwise_params.size()); + LOG("Starting column-wise parameter array binding." + " paramSetSize: {}, paramCount: {}", + paramSetSize, columnwise_params.size()); std::vector> tempBuffers; try { - for (int paramIndex = 0; paramIndex < columnwise_params.size(); ++paramIndex) { + for ( + int paramIndex = 0; + paramIndex < columnwise_params.size(); + ++paramIndex) { const py::list& columnValues = columnwise_params[paramIndex].cast(); const ParamInfo& info = paramInfos[paramIndex]; if (columnValues.size() != paramSetSize) { - ThrowStdException("Column " + std::to_string(paramIndex) + " has mismatched size."); + ThrowStdException( + "Column " + + std::to_string(paramIndex) + + " has mismatched size."); } void* dataPtr = nullptr; SQLLEN* strLenOrIndArray = nullptr; SQLLEN bufferLength = 0; switch (info.paramCType) { case SQL_C_LONG: { - int* dataArray = AllocateParamBufferArray(tempBuffers, paramSetSize); - for (size_t i = 0; i < paramSetSize; ++i) { + int* dataArray = + AllocateParamBufferArray( + tempBuffers, paramSetSize); + for (size_t i = 0; + i < paramSetSize; + ++i) { if (columnValues[i].is_none()) { if (!strLenOrIndArray) - strLenOrIndArray = AllocateParamBufferArray(tempBuffers, paramSetSize); + strLenOrIndArray = + AllocateParamBufferArray( + tempBuffers, paramSetSize); dataArray[i] = 0; strLenOrIndArray[i] = SQL_NULL_DATA; } else { @@ -1328,11 +1652,15 @@ SQLRETURN BindParameterArray(SQLHANDLE hStmt, break; } case SQL_C_DOUBLE: { - double* dataArray = AllocateParamBufferArray(tempBuffers, paramSetSize); + double* dataArray = + AllocateParamBufferArray( + tempBuffers, paramSetSize); for (size_t i = 0; i < paramSetSize; ++i) { if (columnValues[i].is_none()) { if (!strLenOrIndArray) - strLenOrIndArray = AllocateParamBufferArray(tempBuffers, paramSetSize); + strLenOrIndArray = + AllocateParamBufferArray( + tempBuffers, paramSetSize); dataArray[i] = 0; strLenOrIndArray[i] = SQL_NULL_DATA; } else { @@ -1344,19 +1672,34 @@ SQLRETURN BindParameterArray(SQLHANDLE hStmt, break; } case SQL_C_WCHAR: { - SQLWCHAR* wcharArray = AllocateParamBufferArray(tempBuffers, paramSetSize * (info.columnSize + 1)); - strLenOrIndArray = AllocateParamBufferArray(tempBuffers, paramSetSize); + SQLWCHAR* wcharArray = + AllocateParamBufferArray( + tempBuffers, + paramSetSize * (info.columnSize + 1)); + strLenOrIndArray = + AllocateParamBufferArray( + tempBuffers, paramSetSize); for (size_t i = 0; i < paramSetSize; ++i) { if (columnValues[i].is_none()) { strLenOrIndArray[i] = SQL_NULL_DATA; - std::memset(wcharArray + i * (info.columnSize + 1), 0, (info.columnSize + 1) * sizeof(SQLWCHAR)); + std::memset( + wcharArray + i * (info.columnSize + 1), + 0, + (info.columnSize + 1) * sizeof(SQLWCHAR)); } else { - std::wstring wstr = columnValues[i].cast(); + std::wstring wstr = + columnValues[i].cast(); if (wstr.length() > info.columnSize) { std::string offending = WideToUTF8(wstr); - ThrowStdException("Input string exceeds allowed column size at parameter index " + std::to_string(paramIndex)); + ThrowStdException( + "Input string exceeds allowed column" + " size at parameter index " + + std::to_string(paramIndex)); } - std::memcpy(wcharArray + i * (info.columnSize + 1), wstr.c_str(), (wstr.length() + 1) * sizeof(SQLWCHAR)); + std::memcpy( + wcharArray + i * (info.columnSize + 1), + wstr.c_str(), + (wstr.length() + 1) * sizeof(SQLWCHAR)); strLenOrIndArray[i] = SQL_NTS; } } @@ -1366,17 +1709,23 @@ SQLRETURN BindParameterArray(SQLHANDLE hStmt, } case SQL_C_TINYINT: case SQL_C_UTINYINT: { - unsigned char* dataArray = AllocateParamBufferArray(tempBuffers, paramSetSize); + unsigned char* dataArray = + AllocateParamBufferArray( + tempBuffers, paramSetSize); for (size_t i = 0; i < paramSetSize; ++i) { if (columnValues[i].is_none()) { if (!strLenOrIndArray) - strLenOrIndArray = AllocateParamBufferArray(tempBuffers, paramSetSize); + strLenOrIndArray = + AllocateParamBufferArray( + tempBuffers, paramSetSize); dataArray[i] = 0; strLenOrIndArray[i] = SQL_NULL_DATA; } else { int intVal = columnValues[i].cast(); if (intVal < 0 || intVal > 255) { - ThrowStdException("UTINYINT value out of range at rowIndex " + std::to_string(i)); + ThrowStdException( + "UTINYINT value out of range at rowIndex " + + std::to_string(i)); } dataArray[i] = static_cast(intVal); if (strLenOrIndArray) strLenOrIndArray[i] = 0; @@ -1387,40 +1736,58 @@ SQLRETURN BindParameterArray(SQLHANDLE hStmt, break; } case SQL_C_SHORT: { - short* dataArray = AllocateParamBufferArray(tempBuffers, paramSetSize); + int16_t* dataArray = + AllocateParamBufferArray( + tempBuffers, paramSetSize); for (size_t i = 0; i < paramSetSize; ++i) { if (columnValues[i].is_none()) { if (!strLenOrIndArray) - strLenOrIndArray = AllocateParamBufferArray(tempBuffers, paramSetSize); + strLenOrIndArray = + AllocateParamBufferArray( + tempBuffers, paramSetSize); dataArray[i] = 0; strLenOrIndArray[i] = SQL_NULL_DATA; } else { int intVal = columnValues[i].cast(); - if (intVal < std::numeric_limits::min() || - intVal > std::numeric_limits::max()) { - ThrowStdException("SHORT value out of range at rowIndex " + std::to_string(i)); + if (intVal < std::numeric_limits::min() || + intVal > std::numeric_limits::max()) { + ThrowStdException( + "SHORT value out of range at rowIndex " + + std::to_string(i)); } - dataArray[i] = static_cast(intVal); + dataArray[i] = static_cast(intVal); if (strLenOrIndArray) strLenOrIndArray[i] = 0; } } dataPtr = dataArray; - bufferLength = sizeof(short); + bufferLength = sizeof(int16_t); break; } case SQL_C_CHAR: case SQL_C_BINARY: { - char* charArray = AllocateParamBufferArray(tempBuffers, paramSetSize * (info.columnSize + 1)); - strLenOrIndArray = AllocateParamBufferArray(tempBuffers, paramSetSize); + char* charArray = + AllocateParamBufferArray( + tempBuffers, + paramSetSize * (info.columnSize + 1)); + strLenOrIndArray = + AllocateParamBufferArray( + tempBuffers, paramSetSize); for (size_t i = 0; i < paramSetSize; ++i) { if (columnValues[i].is_none()) { strLenOrIndArray[i] = SQL_NULL_DATA; - std::memset(charArray + i * (info.columnSize + 1), 0, info.columnSize + 1); + std::memset( + charArray + i * (info.columnSize + 1), + 0, info.columnSize + 1); } else { - std::string str = columnValues[i].cast(); + std::string str = + columnValues[i].cast(); if (str.size() > info.columnSize) - ThrowStdException("Input exceeds column size at index " + std::to_string(i)); - std::memcpy(charArray + i * (info.columnSize + 1), str.c_str(), str.size()); + ThrowStdException( + "Input exceeds column size at index " + + std::to_string(i)); + std::memcpy( + charArray + i * (info.columnSize + 1), + str.c_str(), str.size()); strLenOrIndArray[i] = static_cast(str.size()); } } @@ -1429,8 +1796,12 @@ SQLRETURN BindParameterArray(SQLHANDLE hStmt, break; } case SQL_C_BIT: { - char* boolArray = AllocateParamBufferArray(tempBuffers, paramSetSize); - strLenOrIndArray = AllocateParamBufferArray(tempBuffers, paramSetSize); + char* boolArray = + AllocateParamBufferArray( + tempBuffers, paramSetSize); + strLenOrIndArray = + AllocateParamBufferArray( + tempBuffers, paramSetSize); for (size_t i = 0; i < paramSetSize; ++i) { if (columnValues[i].is_none()) { boolArray[i] = 0; @@ -1446,27 +1817,35 @@ SQLRETURN BindParameterArray(SQLHANDLE hStmt, } case SQL_C_STINYINT: case SQL_C_USHORT: { - unsigned short* dataArray = AllocateParamBufferArray(tempBuffers, paramSetSize); - strLenOrIndArray = AllocateParamBufferArray(tempBuffers, paramSetSize); + uint16_t* dataArray = + AllocateParamBufferArray( + tempBuffers, paramSetSize); + strLenOrIndArray = + AllocateParamBufferArray( + tempBuffers, paramSetSize); for (size_t i = 0; i < paramSetSize; ++i) { if (columnValues[i].is_none()) { strLenOrIndArray[i] = SQL_NULL_DATA; dataArray[i] = 0; } else { - dataArray[i] = columnValues[i].cast(); + dataArray[i] = columnValues[i].cast(); strLenOrIndArray[i] = 0; } } dataPtr = dataArray; - bufferLength = sizeof(unsigned short); + bufferLength = sizeof(uint16_t); break; } case SQL_C_SBIGINT: case SQL_C_SLONG: case SQL_C_UBIGINT: case SQL_C_ULONG: { - int64_t* dataArray = AllocateParamBufferArray(tempBuffers, paramSetSize); - strLenOrIndArray = AllocateParamBufferArray(tempBuffers, paramSetSize); + int64_t* dataArray = + AllocateParamBufferArray( + tempBuffers, paramSetSize); + strLenOrIndArray = + AllocateParamBufferArray( + tempBuffers, paramSetSize); for (size_t i = 0; i < paramSetSize; ++i) { if (columnValues[i].is_none()) { strLenOrIndArray[i] = SQL_NULL_DATA; @@ -1481,8 +1860,12 @@ SQLRETURN BindParameterArray(SQLHANDLE hStmt, break; } case SQL_C_FLOAT: { - float* dataArray = AllocateParamBufferArray(tempBuffers, paramSetSize); - strLenOrIndArray = AllocateParamBufferArray(tempBuffers, paramSetSize); + float* dataArray = + AllocateParamBufferArray( + tempBuffers, paramSetSize); + strLenOrIndArray = + AllocateParamBufferArray( + tempBuffers, paramSetSize); for (size_t i = 0; i < paramSetSize; ++i) { if (columnValues[i].is_none()) { strLenOrIndArray[i] = SQL_NULL_DATA; @@ -1497,17 +1880,24 @@ SQLRETURN BindParameterArray(SQLHANDLE hStmt, break; } case SQL_C_TYPE_DATE: { - SQL_DATE_STRUCT* dateArray = AllocateParamBufferArray(tempBuffers, paramSetSize); - strLenOrIndArray = AllocateParamBufferArray(tempBuffers, paramSetSize); + SQL_DATE_STRUCT* dateArray = + AllocateParamBufferArray( + tempBuffers, paramSetSize); + strLenOrIndArray = + AllocateParamBufferArray( + tempBuffers, paramSetSize); for (size_t i = 0; i < paramSetSize; ++i) { if (columnValues[i].is_none()) { strLenOrIndArray[i] = SQL_NULL_DATA; std::memset(&dateArray[i], 0, sizeof(SQL_DATE_STRUCT)); } else { py::object dateObj = columnValues[i]; - dateArray[i].year = dateObj.attr("year").cast(); - dateArray[i].month = dateObj.attr("month").cast(); - dateArray[i].day = dateObj.attr("day").cast(); + dateArray[i].year = + dateObj.attr("year").cast(); + dateArray[i].month = + dateObj.attr("month").cast(); + dateArray[i].day = + dateObj.attr("day").cast(); strLenOrIndArray[i] = 0; } } @@ -1516,17 +1906,25 @@ SQLRETURN BindParameterArray(SQLHANDLE hStmt, break; } case SQL_C_TYPE_TIME: { - SQL_TIME_STRUCT* timeArray = AllocateParamBufferArray(tempBuffers, paramSetSize); - strLenOrIndArray = AllocateParamBufferArray(tempBuffers, paramSetSize); + SQL_TIME_STRUCT* timeArray = + AllocateParamBufferArray( + tempBuffers, paramSetSize); + strLenOrIndArray = + AllocateParamBufferArray( + tempBuffers, paramSetSize); for (size_t i = 0; i < paramSetSize; ++i) { if (columnValues[i].is_none()) { strLenOrIndArray[i] = SQL_NULL_DATA; - std::memset(&timeArray[i], 0, sizeof(SQL_TIME_STRUCT)); + std::memset(&timeArray[i], 0, + sizeof(SQL_TIME_STRUCT)); } else { py::object timeObj = columnValues[i]; - timeArray[i].hour = timeObj.attr("hour").cast(); - timeArray[i].minute = timeObj.attr("minute").cast(); - timeArray[i].second = timeObj.attr("second").cast(); + timeArray[i].hour = + timeObj.attr("hour").cast(); + timeArray[i].minute = + timeObj.attr("minute").cast(); + timeArray[i].second = + timeObj.attr("second").cast(); strLenOrIndArray[i] = 0; } } @@ -1535,21 +1933,34 @@ SQLRETURN BindParameterArray(SQLHANDLE hStmt, break; } case SQL_C_TYPE_TIMESTAMP: { - SQL_TIMESTAMP_STRUCT* tsArray = AllocateParamBufferArray(tempBuffers, paramSetSize); - strLenOrIndArray = AllocateParamBufferArray(tempBuffers, paramSetSize); + SQL_TIMESTAMP_STRUCT* tsArray = + AllocateParamBufferArray( + tempBuffers, paramSetSize); + strLenOrIndArray = + AllocateParamBufferArray( + tempBuffers, paramSetSize); for (size_t i = 0; i < paramSetSize; ++i) { if (columnValues[i].is_none()) { strLenOrIndArray[i] = SQL_NULL_DATA; - std::memset(&tsArray[i], 0, sizeof(SQL_TIMESTAMP_STRUCT)); + std::memset(&tsArray[i], 0, + sizeof(SQL_TIMESTAMP_STRUCT)); } else { py::object dtObj = columnValues[i]; - tsArray[i].year = dtObj.attr("year").cast(); - tsArray[i].month = dtObj.attr("month").cast(); - tsArray[i].day = dtObj.attr("day").cast(); - tsArray[i].hour = dtObj.attr("hour").cast(); - tsArray[i].minute = dtObj.attr("minute").cast(); - tsArray[i].second = dtObj.attr("second").cast(); - tsArray[i].fraction = static_cast(dtObj.attr("microsecond").cast() * 1000); // µs to ns + tsArray[i].year = + dtObj.attr("year").cast(); + tsArray[i].month = + dtObj.attr("month").cast(); + tsArray[i].day = + dtObj.attr("day").cast(); + tsArray[i].hour = + dtObj.attr("hour").cast(); + tsArray[i].minute = + dtObj.attr("minute").cast(); + tsArray[i].second = + dtObj.attr("second").cast(); + // µs to ns + tsArray[i].fraction = static_cast( + dtObj.attr("microsecond").cast() * 1000); strLenOrIndArray[i] = 0; } } @@ -1558,28 +1969,41 @@ SQLRETURN BindParameterArray(SQLHANDLE hStmt, break; } case SQL_C_NUMERIC: { - SQL_NUMERIC_STRUCT* numericArray = AllocateParamBufferArray(tempBuffers, paramSetSize); - strLenOrIndArray = AllocateParamBufferArray(tempBuffers, paramSetSize); + SQL_NUMERIC_STRUCT* numericArray = + AllocateParamBufferArray( + tempBuffers, paramSetSize); + strLenOrIndArray = + AllocateParamBufferArray( + tempBuffers, paramSetSize); for (size_t i = 0; i < paramSetSize; ++i) { const py::handle& element = columnValues[i]; if (element.is_none()) { strLenOrIndArray[i] = SQL_NULL_DATA; - std::memset(&numericArray[i], 0, sizeof(SQL_NUMERIC_STRUCT)); + std::memset(&numericArray[i], 0, + sizeof(SQL_NUMERIC_STRUCT)); continue; } if (!py::isinstance(element)) { - throw std::runtime_error(MakeParamMismatchErrorStr(info.paramCType, paramIndex)); + throw std::runtime_error( + MakeParamMismatchErrorStr( + info.paramCType, paramIndex)); } NumericData decimalParam = element.cast(); - LOG("Received numeric parameter at [%zu]: precision=%d, scale=%d, sign=%d, val=%lld", - i, decimalParam.precision, decimalParam.scale, decimalParam.sign, decimalParam.val); + LOG( + "Received numeric parameter at [%zu]: + precision=%d, scale=%d, sign=%d, val=%lld", + i, decimalParam.precision, + decimalParam.scale, decimalParam.sign, + decimalParam.val); numericArray[i].precision = decimalParam.precision; numericArray[i].scale = decimalParam.scale; numericArray[i].sign = decimalParam.sign; - std::memset(numericArray[i].val, 0, sizeof(numericArray[i].val)); + std::memset(numericArray[i].val, 0, + sizeof(numericArray[i].val)); std::memcpy(numericArray[i].val, - reinterpret_cast(&decimalParam.val), - std::min(sizeof(decimalParam.val), sizeof(numericArray[i].val))); + reinterpret_cast(&decimalParam.val), + std::min(sizeof(decimalParam.val), + sizeof(numericArray[i].val))); strLenOrIndArray[i] = sizeof(SQL_NUMERIC_STRUCT); } dataPtr = numericArray; @@ -1587,7 +2011,9 @@ SQLRETURN BindParameterArray(SQLHANDLE hStmt, break; } default: { - ThrowStdException("BindParameterArray: Unsupported C type: " + std::to_string(info.paramCType)); + ThrowStdException( + "BindParameterArray: Unsupported C type: " + + std::to_string(info.paramCType)); } } RETCODE rc = SQLBindParameter_ptr( @@ -1600,18 +2026,21 @@ SQLRETURN BindParameterArray(SQLHANDLE hStmt, info.decimalDigits, dataPtr, bufferLength, - strLenOrIndArray - ); + strLenOrIndArray); if (!SQL_SUCCEEDED(rc)) { LOG("Failed to bind array param {}", paramIndex); return rc; } } } catch (...) { - LOG("Exception occurred during parameter array binding. Cleaning up."); + LOG("Exception occurred during parameter " + "array binding. Cleaning up."); throw; } - paramBuffers.insert(paramBuffers.end(), tempBuffers.begin(), tempBuffers.end()); + paramBuffers.insert( + paramBuffers.end(), + tempBuffers.begin(), + tempBuffers.end()); LOG("Finished column-wise parameter array binding."); return SQL_SUCCESS; } @@ -1632,9 +2061,14 @@ SQLRETURN SQLExecuteMany_wrap(const SqlHandlePtr statementHandle, RETCODE rc = SQLPrepare_ptr(hStmt, queryPtr, SQL_NTS); if (!SQL_SUCCEEDED(rc)) return rc; std::vector> paramBuffers; - rc = BindParameterArray(hStmt, columnwise_params, paramInfos, paramSetSize, paramBuffers); + rc = BindParameterArray( + hStmt, columnwise_params, + paramInfos, paramSetSize, + paramBuffers); if (!SQL_SUCCEEDED(rc)) return rc; - rc = SQLSetStmtAttr_ptr(hStmt, SQL_ATTR_PARAMSET_SIZE, (SQLPOINTER)paramSetSize, 0); + rc = SQLSetStmtAttr_ptr( + hStmt, SQL_ATTR_PARAMSET_SIZE, + (SQLPOINTER)paramSetSize, 0); if (!SQL_SUCCEEDED(rc)) return rc; rc = SQLExecute_ptr(hStmt); return rc; @@ -1644,8 +2078,10 @@ SQLRETURN SQLExecuteMany_wrap(const SqlHandlePtr statementHandle, SQLSMALLINT SQLNumResultCols_wrap(SqlHandlePtr statementHandle) { LOG("Get number of columns in result set"); if (!SQLNumResultCols_ptr) { - LOG("Function pointer not initialized. Loading the driver."); - DriverLoader::getInstance().loadDriver(); // Load the driver + LOG("Function pointer not initialized. " + "Loading the driver."); + // Load the driver + DriverLoader::getInstance().loadDriver(); } SQLSMALLINT columnCount; @@ -1655,11 +2091,14 @@ SQLSMALLINT SQLNumResultCols_wrap(SqlHandlePtr statementHandle) { } // Wrap SQLDescribeCol -SQLRETURN SQLDescribeCol_wrap(SqlHandlePtr StatementHandle, py::list& ColumnMetadata) { +SQLRETURN SQLDescribeCol_wrap( + SqlHandlePtr StatementHandle, + py::list& ColumnMetadata) { LOG("Get column description"); if (!SQLDescribeCol_ptr) { LOG("Function pointer not initialized. Loading the driver."); - DriverLoader::getInstance().loadDriver(); // Load the driver + // Load the driver + DriverLoader::getInstance().loadDriver(); } SQLSMALLINT ColumnCount; @@ -1679,20 +2118,25 @@ SQLRETURN SQLDescribeCol_wrap(SqlHandlePtr StatementHandle, py::list& ColumnMeta SQLSMALLINT Nullable; retcode = SQLDescribeCol_ptr(StatementHandle->get(), i, ColumnName, - sizeof(ColumnName) / sizeof(SQLWCHAR), &NameLength, &DataType, + sizeof(ColumnName) / sizeof(SQLWCHAR), + &NameLength, &DataType, &ColumnSize, &DecimalDigits, &Nullable); if (SQL_SUCCEEDED(retcode)) { // Append a named py::dict to ColumnMetadata // TODO: Should we define a struct for this task instead of dict? #if defined(__APPLE__) || defined(__linux__) - ColumnMetadata.append(py::dict("ColumnName"_a = SQLWCHARToWString(ColumnName, SQL_NTS), + ColumnMetadata.append( + py::dict("ColumnName"_a = + SQLWCHARToWString(ColumnName, SQL_NTS), #else - ColumnMetadata.append(py::dict("ColumnName"_a = std::wstring(ColumnName), + ColumnMetadata.append( + py::dict("ColumnName"_a = + std::wstring(ColumnName), #endif - "DataType"_a = DataType, "ColumnSize"_a = ColumnSize, - "DecimalDigits"_a = DecimalDigits, - "Nullable"_a = Nullable)); + "DataType"_a = DataType, "ColumnSize"_a = ColumnSize, + "DecimalDigits"_a = DecimalDigits, + "Nullable"_a = Nullable)); } else { return retcode; } @@ -1704,7 +2148,8 @@ SQLRETURN SQLDescribeCol_wrap(SqlHandlePtr StatementHandle, py::list& ColumnMeta SQLRETURN SQLFetch_wrap(SqlHandlePtr StatementHandle) { LOG("Fetch next row"); if (!SQLFetch_ptr) { - LOG("Function pointer not initialized. Loading the driver."); + LOG("Function pointer not initialized." + " Loading the driver."); DriverLoader::getInstance().loadDriver(); // Load the driver } @@ -1713,11 +2158,14 @@ SQLRETURN SQLFetch_wrap(SqlHandlePtr StatementHandle) { // Helper function to retrieve column data // TODO: Handle variable length data correctly -SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, py::list& row) { +SQLRETURN SQLGetData_wrap( + SqlHandlePtr StatementHandle, + SQLUSMALLINT colCount, py::list& row) { LOG("Get data from columns"); if (!SQLGetData_ptr) { LOG("Function pointer not initialized. Loading the driver."); - DriverLoader::getInstance().loadDriver(); // Load the driver + // Load the driver + DriverLoader::getInstance().loadDriver(); } SQLRETURN ret = SQL_SUCCESS; @@ -1730,10 +2178,15 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p SQLSMALLINT decimalDigits; SQLSMALLINT nullable; - ret = SQLDescribeCol_ptr(hStmt, i, columnName, sizeof(columnName) / sizeof(SQLWCHAR), - &columnNameLen, &dataType, &columnSize, &decimalDigits, &nullable); + ret = SQLDescribeCol_ptr( + hStmt, i, columnName, + sizeof(columnName) / sizeof(SQLWCHAR), + &columnNameLen, &dataType, + &columnSize, &decimalDigits, &nullable); if (!SQL_SUCCEEDED(ret)) { - LOG("Error retrieving data for column - {}, SQLDescribeCol return code - {}", i, ret); + LOG( + "Error retrieving data for column - {}, " + "SQLDescribeCol return code - {}", i, ret); row.append(py::none()); // TODO: Do we want to continue in this case or return? continue; @@ -1745,112 +2198,158 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p case SQL_LONGVARCHAR: { // TODO: revisit HandleZeroColumnSizeAtFetch(columnSize); - uint64_t fetchBufferSize = columnSize + 1 /* null-termination */; + // null terminator + uint64_t fetchBufferSize = columnSize + 1; std::vector dataBuffer(fetchBufferSize); SQLLEN dataLen; // TODO: Handle the return code better - ret = SQLGetData_ptr(hStmt, i, SQL_C_CHAR, dataBuffer.data(), dataBuffer.size(), - &dataLen); + ret = SQLGetData_ptr( + hStmt, i, SQL_C_CHAR, + dataBuffer.data(), dataBuffer.size(), + &dataLen); if (SQL_SUCCEEDED(ret)) { - // TODO: Refactor these if's across other switches to avoid code duplication + // TODO: Refactor these if's across + // other switches to avoid code duplication // columnSize is in chars, dataLen is in bytes if (dataLen > 0) { - uint64_t numCharsInData = dataLen / sizeof(SQLCHAR); - // NOTE: dataBuffer.size() includes null-terminator, dataLen doesn't. Hence use '<'. - if (numCharsInData < dataBuffer.size()) { + uint64_t numCharsInData = + dataLen / sizeof(SQLCHAR); + // NOTE: dataBuffer.size() includes + // null-terminator, dataLen doesn't. + // Hence use '<'. + if (numCharsInData < dataBuffer.size()) { // SQLGetData will null-terminate the data #if defined(__APPLE__) || defined(__linux__) - std::string fullStr(reinterpret_cast(dataBuffer.data())); + std::string fullStr( + reinterpret_cast( + dataBuffer.data())); row.append(fullStr); - LOG("macOS/Linux: Appended CHAR string of length {} to result row", fullStr.length()); + LOG( + "macOS/Linux: Appended CHAR string of " + "length {} to result row", + fullStr.length()); #else - row.append(std::string(reinterpret_cast(dataBuffer.data()))); + row.append( + std::string( + reinterpret_cast( + dataBuffer.data()))); #endif - } else { - // In this case, buffer size is smaller, and data to be retrieved is longer + } else { + // In this case, buffer size is smaller, + // and data to be retrieved is longer // TODO: Revisit std::ostringstream oss; - oss << "Buffer length for fetch (" << dataBuffer.size()-1 << ") is smaller, & data " - << "to be retrieved is longer (" << numCharsInData << "). ColumnID - " - << i << ", datatype - " << dataType; + oss << "Buffer length for fetch (" << + dataBuffer.size()-1 << + ") is smaller, & data " + << "to be retrieved is longer (" + << numCharsInData << "). ColumnID - " + << i << ", datatype - " + << dataType; ThrowStdException(oss.str()); } - } else if (dataLen == SQL_NULL_DATA) { - row.append(py::none()); + } else if (dataLen == SQL_NULL_DATA) { + row.append(py::none()); } else { assert(dataLen == SQL_NO_TOTAL); - LOG("SQLGetData couldn't determine the length of the data. " - "Returning NULL value instead. Column ID - {}", i); - row.append(py::none()); + LOG("SQLGetData couldn't determine " + "the length of the data. " + "Returning NULL value instead. " + "Column ID - {}", i); + row.append(py::none()); } - } else { - LOG("Error retrieving data for column - {}, data type - {}, SQLGetData return " - "code - {}. Returning NULL value instead", - i, dataType, ret); - row.append(py::none()); - } + } else { + LOG("Error retrieving data for column - {}," + " data type - {}, SQLGetData return " + "code - {}. Returning NULL value instead", + i, dataType, ret); + row.append(py::none()); + } break; } case SQL_WCHAR: case SQL_WVARCHAR: - case SQL_WLONGVARCHAR: { + case SQL_WLONGVARCHAR: { // TODO: revisit HandleZeroColumnSizeAtFetch(columnSize); - uint64_t fetchBufferSize = columnSize + 1 /* null-termination */; + // null terminator + uint64_t fetchBufferSize = columnSize + 1; std::vector dataBuffer(fetchBufferSize); SQLLEN dataLen; - ret = SQLGetData_ptr(hStmt, i, SQL_C_WCHAR, dataBuffer.data(), - dataBuffer.size() * sizeof(SQLWCHAR), &dataLen); + ret = SQLGetData_ptr( + hStmt, i, SQL_C_WCHAR, + dataBuffer.data(), + dataBuffer.size() * sizeof(SQLWCHAR), + &dataLen); if (SQL_SUCCEEDED(ret)) { - // TODO: Refactor these if's across other switches to avoid code duplication + // TODO: Refactor these if's across other + // switches to avoid code duplication if (dataLen > 0) { - uint64_t numCharsInData = dataLen / sizeof(SQLWCHAR); - if (numCharsInData < dataBuffer.size()) { + uint64_t numCharsInData = + dataLen / sizeof(SQLWCHAR); + if (numCharsInData < dataBuffer.size()) { // SQLGetData will null-terminate the data #if defined(__APPLE__) || defined(__linux__) - auto raw_bytes = reinterpret_cast(dataBuffer.data()); - size_t actualBufferSize = dataBuffer.size() * sizeof(SQLWCHAR); - if (dataLen < 0 || static_cast(dataLen) > actualBufferSize) { - LOG("Error: py::bytes creation request exceeds buffer size. dataLen={} buffer={}", + auto raw_bytes = + reinterpret_cast( + dataBuffer.data()); + size_t actualBufferSize = + dataBuffer.size() * sizeof(SQLWCHAR); + if (dataLen < 0 || + static_cast(dataLen) > actualBufferSize) { + LOG( + "Error: py::bytes creation request" + " exceeds buffer size. dataLen={} " + "buffer={}", dataLen, actualBufferSize); - ThrowStdException("Invalid buffer length for py::bytes"); + ThrowStdException( + "Invalid buffer length for py::bytes"); } py::bytes py_bytes(raw_bytes, dataLen); - py::str decoded = py_bytes.attr("decode")("utf-16-le"); + py::str decoded = + py_bytes.attr("decode")("utf-16-le"); row.append(decoded); #else row.append(std::wstring(dataBuffer.data())); #endif - } else { - // In this case, buffer size is smaller, and data to be retrieved is longer + } else { + // In this case, buffer size is + // smaller, and data to be retrieved is longer // TODO: Revisit std::ostringstream oss; - oss << "Buffer length for fetch (" << dataBuffer.size()-1 << ") is smaller, & data " - << "to be retrieved is longer (" << numCharsInData << "). ColumnID - " + oss << "Buffer length for fetch (" << + dataBuffer.size()-1 << ") is smaller, & data " + << "to be retrieved is longer (" << + numCharsInData << "). ColumnID - " << i << ", datatype - " << dataType; ThrowStdException(oss.str()); } - } else if (dataLen == SQL_NULL_DATA) { - row.append(py::none()); + } else if (dataLen == SQL_NULL_DATA) { + row.append(py::none()); } else { assert(dataLen == SQL_NO_TOTAL); - LOG("SQLGetData couldn't determine the length of the data. " - "Returning NULL value instead. Column ID - {}", i); - row.append(py::none()); + LOG("SQLGetData couldn't determine the" + " length of the data. " + "Returning NULL value instead. Column ID" + " - {}", i); + row.append(py::none()); } - } else { - LOG("Error retrieving data for column - {}, data type - {}, SQLGetData return " - "code - {}. Returning NULL value instead", - i, dataType, ret); - row.append(py::none()); - } + } else { + LOG("Error retrieving data for column - {}," + " data type - {}, SQLGetData return " + "code - {}. Returning NULL value instead", + i, dataType, ret); + row.append(py::none()); + } break; } case SQL_INTEGER: { SQLINTEGER intValue; - ret = SQLGetData_ptr(hStmt, i, SQL_C_LONG, &intValue, 0, NULL); + ret = SQLGetData_ptr( + hStmt, i, SQL_C_LONG, + &intValue, 0, NULL); if (SQL_SUCCEEDED(ret)) { row.append(static_cast(intValue)); } else { @@ -1860,11 +2359,14 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p } case SQL_SMALLINT: { SQLSMALLINT smallIntValue; - ret = SQLGetData_ptr(hStmt, i, SQL_C_SHORT, &smallIntValue, 0, NULL); + ret = SQLGetData_ptr( + hStmt, i, SQL_C_SHORT, + &smallIntValue, 0, NULL); if (SQL_SUCCEEDED(ret)) { row.append(static_cast(smallIntValue)); } else { - LOG("Error retrieving data for column - {}, data type - {}, SQLGetData return " + LOG("Error retrieving data for column - {}," + " data type - {}, SQLGetData return " "code - {}. Returning NULL value instead", i, dataType, ret); row.append(py::none()); @@ -1873,11 +2375,14 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p } case SQL_REAL: { SQLREAL realValue; - ret = SQLGetData_ptr(hStmt, i, SQL_C_FLOAT, &realValue, 0, NULL); + ret = SQLGetData_ptr( + hStmt, i, SQL_C_FLOAT, + &realValue, 0, NULL); if (SQL_SUCCEEDED(ret)) { row.append(realValue); } else { - LOG("Error retrieving data for column - {}, data type - {}, SQLGetData return " + LOG("Error retrieving data for column - {}," + " data type - {}, SQLGetData return " "code - {}. Returning NULL value instead", i, dataType, ret); row.append(py::none()); @@ -1888,21 +2393,27 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p case SQL_NUMERIC: { SQLCHAR numericStr[MAX_DIGITS_IN_NUMERIC] = {0}; SQLLEN indicator; - ret = SQLGetData_ptr(hStmt, i, SQL_C_CHAR, numericStr, sizeof(numericStr), &indicator); + ret = SQLGetData_ptr( + hStmt, i, SQL_C_CHAR, + numericStr, sizeof(numericStr), + &indicator); if (SQL_SUCCEEDED(ret)) { - try{ + try { // Convert numericStr to py::decimal.Decimal and append to row - row.append(py::module_::import("decimal").attr("Decimal")( - std::string(reinterpret_cast(numericStr), indicator))); + row.append( + py::module_::import("decimal").attr("Decimal")( + std::string( + reinterpret_cast(numericStr), + indicator))); } catch (const py::error_already_set& e) { // If the conversion fails, append None LOG("Error converting to decimal: {}", e.what()); row.append(py::none()); } - } - else { - LOG("Error retrieving data for column - {}, data type - {}, SQLGetData return " + } else { + LOG("Error retrieving data for column - {}," + " data type - {}, SQLGetData return " "code - {}. Returning NULL value instead", i, dataType, ret); row.append(py::none()); @@ -1912,11 +2423,14 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p case SQL_DOUBLE: case SQL_FLOAT: { SQLDOUBLE doubleValue; - ret = SQLGetData_ptr(hStmt, i, SQL_C_DOUBLE, &doubleValue, 0, NULL); + ret = SQLGetData_ptr( + hStmt, i, SQL_C_DOUBLE, + &doubleValue, 0, NULL); if (SQL_SUCCEEDED(ret)) { row.append(doubleValue); } else { - LOG("Error retrieving data for column - {}, data type - {}, SQLGetData return " + LOG("Error retrieving data for column - {}," + " data type - {}, SQLGetData return " "code - {}. Returning NULL value instead", i, dataType, ret); row.append(py::none()); @@ -1925,11 +2439,14 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p } case SQL_BIGINT: { SQLBIGINT bigintValue; - ret = SQLGetData_ptr(hStmt, i, SQL_C_SBIGINT, &bigintValue, 0, NULL); + ret = SQLGetData_ptr( + hStmt, i, SQL_C_SBIGINT, + &bigintValue, 0, NULL); if (SQL_SUCCEEDED(ret)) { - row.append(static_cast(bigintValue)); + row.append(static_cast(bigintValue)); } else { - LOG("Error retrieving data for column - {}, data type - {}, SQLGetData return " + LOG("Error retrieving data for column - {}," + " data type - {}, SQLGetData return " "code - {}. Returning NULL value instead", i, dataType, ret); row.append(py::none()); @@ -1939,17 +2456,19 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p case SQL_TYPE_DATE: { SQL_DATE_STRUCT dateValue; ret = - SQLGetData_ptr(hStmt, i, SQL_C_TYPE_DATE, &dateValue, sizeof(dateValue), NULL); + SQLGetData_ptr( + hStmt, i, SQL_C_TYPE_DATE, + &dateValue, sizeof(dateValue), + NULL); if (SQL_SUCCEEDED(ret)) { row.append( py::module_::import("datetime").attr("date")( dateValue.year, dateValue.month, - dateValue.day - ) - ); + dateValue.day)); } else { - LOG("Error retrieving data for column - {}, data type - {}, SQLGetData return " + LOG("Error retrieving data for column - {}," + " data type - {}, SQLGetData return " "code - {}. Returning NULL value instead", i, dataType, ret); row.append(py::none()); @@ -1961,17 +2480,19 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p case SQL_SS_TIME2: { SQL_TIME_STRUCT timeValue; ret = - SQLGetData_ptr(hStmt, i, SQL_C_TYPE_TIME, &timeValue, sizeof(timeValue), NULL); + SQLGetData_ptr( + hStmt, i, SQL_C_TYPE_TIME, + &timeValue, sizeof(timeValue), + NULL); if (SQL_SUCCEEDED(ret)) { row.append( py::module_::import("datetime").attr("time")( timeValue.hour, timeValue.minute, - timeValue.second - ) - ); + timeValue.second)); } else { - LOG("Error retrieving data for column - {}, data type - {}, SQLGetData return " + LOG("Error retrieving data for column - {}," + " data type - {}, SQLGetData return " "code - {}. Returning NULL value instead", i, dataType, ret); row.append(py::none()); @@ -1982,8 +2503,11 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p case SQL_TYPE_TIMESTAMP: case SQL_DATETIME: { SQL_TIMESTAMP_STRUCT timestampValue; - ret = SQLGetData_ptr(hStmt, i, SQL_C_TYPE_TIMESTAMP, ×tampValue, - sizeof(timestampValue), NULL); + ret = SQLGetData_ptr( + hStmt, i, + SQL_C_TYPE_TIMESTAMP, + ×tampValue, + sizeof(timestampValue), NULL); if (SQL_SUCCEEDED(ret)) { row.append( py::module_::import("datetime").attr("datetime")( @@ -1993,11 +2517,11 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p timestampValue.hour, timestampValue.minute, timestampValue.second, - timestampValue.fraction / 1000 // Convert back ns to µs - ) - ); + // Convert back ns to µs + timestampValue.fraction / 1000)); } else { - LOG("Error retrieving data for column - {}, data type - {}, SQLGetData return " + LOG("Error retrieving data for column - {}," + " data type - {}, SQLGetData return " "code - {}. Returning NULL value instead", i, dataType, ret); row.append(py::none()); @@ -2011,46 +2535,60 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p HandleZeroColumnSizeAtFetch(columnSize); std::unique_ptr dataBuffer(new SQLCHAR[columnSize]); SQLLEN dataLen; - ret = SQLGetData_ptr(hStmt, i, SQL_C_BINARY, dataBuffer.get(), columnSize, &dataLen); + ret = SQLGetData_ptr( + hStmt, i, SQL_C_BINARY, + dataBuffer.get(), columnSize, + &dataLen); if (SQL_SUCCEEDED(ret)) { - // TODO: Refactor these if's across other switches to avoid code duplication + // TODO: Refactor these if's across other + // switches to avoid code duplication if (dataLen > 0) { - if (static_cast(dataLen) <= columnSize) { + if (static_cast(dataLen) <= columnSize) { row.append(py::bytes(reinterpret_cast( dataBuffer.get()), dataLen)); - } else { - // In this case, buffer size is smaller, and data to be retrieved is longer + } else { + // In this case, buffer size is smaller, + // and data to be retrieved is longer // TODO: Revisit std::ostringstream oss; - oss << "Buffer length for fetch (" << columnSize << ") is smaller, & data " - << "to be retrieved is longer (" << dataLen << "). ColumnID - " - << i << ", datatype - " << dataType; + oss << "Buffer length for fetch (" << + columnSize << ") is smaller, & data " + << "to be retrieved is longer (" << + dataLen << "). ColumnID - " + << i << ", datatype - " << + dataType; ThrowStdException(oss.str()); } - } else if (dataLen == SQL_NULL_DATA) { - row.append(py::none()); + } else if (dataLen == SQL_NULL_DATA) { + row.append(py::none()); } else { assert(dataLen == SQL_NO_TOTAL); - LOG("SQLGetData couldn't determine the length of the data. " - "Returning NULL value instead. Column ID - {}", i); - row.append(py::none()); + LOG("SQLGetData couldn't determine the" + " length of the data. " + "Returning NULL value instead. " + "Column ID - {}", i); + row.append(py::none()); } - } else { - LOG("Error retrieving data for column - {}, data type - {}, SQLGetData return " - "code - {}. Returning NULL value instead", - i, dataType, ret); - row.append(py::none()); - } + } else { + LOG("Error retrieving data for column - {}," + " data type - {}, SQLGetData return " + "code - {}. Returning NULL value instead", + i, dataType, ret); + row.append(py::none()); + } break; } case SQL_TINYINT: { SQLCHAR tinyIntValue; - ret = SQLGetData_ptr(hStmt, i, SQL_C_TINYINT, &tinyIntValue, 0, NULL); + ret = SQLGetData_ptr( + hStmt, i, SQL_C_TINYINT, + &tinyIntValue, 0, NULL); if (SQL_SUCCEEDED(ret)) { row.append(static_cast(tinyIntValue)); } else { - LOG("Error retrieving data for column - {}, data type - {}, SQLGetData return " + LOG("Error retrieving data for column - {}," + " data type - {}, SQLGetData return " "code - {}. Returning NULL value instead", i, dataType, ret); row.append(py::none()); @@ -2059,11 +2597,14 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p } case SQL_BIT: { SQLCHAR bitValue; - ret = SQLGetData_ptr(hStmt, i, SQL_C_BIT, &bitValue, 0, NULL); + ret = SQLGetData_ptr( + hStmt, i, SQL_C_BIT, + &bitValue, 0, NULL); if (SQL_SUCCEEDED(ret)) { row.append(static_cast(bitValue)); } else { - LOG("Error retrieving data for column - {}, data type - {}, SQLGetData return " + LOG("Error retrieving data for column - {}," + " data type - {}, SQLGetData return " "code - {}. Returning NULL value instead", i, dataType, ret); row.append(py::none()); @@ -2073,22 +2614,36 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p #if (ODBCVER >= 0x0350) case SQL_GUID: { SQLGUID guidValue; - ret = SQLGetData_ptr(hStmt, i, SQL_C_GUID, &guidValue, sizeof(guidValue), NULL); + ret = SQLGetData_ptr( + hStmt, i, SQL_C_GUID, + &guidValue, sizeof(guidValue), + NULL); if (SQL_SUCCEEDED(ret)) { std::ostringstream oss; - oss << std::hex << std::setfill('0') << std::setw(8) << guidValue.Data1 << '-' - << std::setw(4) << guidValue.Data2 << '-' << std::setw(4) << guidValue.Data3 - << '-' << std::setw(2) << static_cast(guidValue.Data4[0]) - << std::setw(2) << static_cast(guidValue.Data4[1]) << '-' << std::hex - << std::setw(2) << static_cast(guidValue.Data4[2]) << std::setw(2) - << static_cast(guidValue.Data4[3]) << std::setw(2) - << static_cast(guidValue.Data4[4]) << std::setw(2) - << static_cast(guidValue.Data4[5]) << std::setw(2) - << static_cast(guidValue.Data4[6]) << std::setw(2) + oss << std::hex << std::setfill('0') + << std::setw(8) << guidValue.Data1 << '-' + << std::setw(4) << guidValue.Data2 << '-' + << std::setw(4) << guidValue.Data3 << '-' + << std::setw(2) + << static_cast(guidValue.Data4[0]) + << std::setw(2) + << static_cast(guidValue.Data4[1]) + << '-' << std::hex << std::setw(2) + << static_cast(guidValue.Data4[2]) + << std::setw(2) + << static_cast(guidValue.Data4[3]) + << std::setw(2) + << static_cast(guidValue.Data4[4]) + << std::setw(2) + << static_cast(guidValue.Data4[5]) + << std::setw(2) + << static_cast(guidValue.Data4[6]) + << std::setw(2) << static_cast(guidValue.Data4[7]); row.append(oss.str()); // Append GUID as a string } else { - LOG("Error retrieving data for column - {}, data type - {}, SQLGetData return " + LOG("Error retrieving data for column - {}," + " data type - {}, SQLGetData return " "code - {}. Returning NULL value instead", i, dataType, ret); row.append(py::none()); @@ -2098,8 +2653,10 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p #endif default: std::ostringstream errorString; - errorString << "Unsupported data type for column - " << columnName << ", Type - " - << dataType << ", column ID - " << i; + errorString << "Unsupported data type for column - " + << columnName << ", Type - " + << dataType << ", column ID - " + << i; LOG(errorString.str()); ThrowStdException(errorString.str()); break; @@ -2108,173 +2665,241 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p return ret; } -SQLRETURN SQLFetchScroll_wrap(SqlHandlePtr StatementHandle, SQLSMALLINT FetchOrientation, SQLLEN FetchOffset, py::list& row_data) { - LOG("Fetching with scroll: orientation={}, offset={}", FetchOrientation, FetchOffset); +SQLRETURN SQLFetchScroll_wrap( + SqlHandlePtr StatementHandle, + SQLSMALLINT FetchOrientation, + SQLLEN FetchOffset, + py::list& row_data) { + LOG("Fetching with scroll: orientation={}, offset={}", + FetchOrientation, FetchOffset); if (!SQLFetchScroll_ptr) { - LOG("Function pointer not initialized. Loading the driver."); - DriverLoader::getInstance().loadDriver(); // Load the driver + LOG("Function pointer not initialized." + " Loading the driver."); + // Load the driver + DriverLoader::getInstance().loadDriver(); } - // Unbind any columns from previous fetch operations to avoid memory corruption + // Unbind any columns from previous + // fetch operations to avoid memory corruption SQLFreeStmt_ptr(StatementHandle->get(), SQL_UNBIND); - + // Perform scroll operation - SQLRETURN ret = SQLFetchScroll_ptr(StatementHandle->get(), FetchOrientation, FetchOffset); - + SQLRETURN ret = SQLFetchScroll_ptr( + StatementHandle->get(), + FetchOrientation, FetchOffset); + // If successful and caller wants data, retrieve it if (SQL_SUCCEEDED(ret) && row_data.size() == 0) { // Get column count SQLSMALLINT colCount = SQLNumResultCols_wrap(StatementHandle); - + // Get the data in a consistent way with other fetch methods ret = SQLGetData_wrap(StatementHandle, colCount, row_data); } - + return ret; } // For column in the result set, binds a buffer to retrieve column data // TODO: Move to anonymous namespace, since it is not used outside this file -SQLRETURN SQLBindColums(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& columnNames, +SQLRETURN SQLBindColums(SQLHSTMT hStmt, ColumnBuffers& buffers, + py::list& columnNames, SQLUSMALLINT numCols, int fetchSize) { SQLRETURN ret = SQL_SUCCESS; // Bind columns based on their data types for (SQLUSMALLINT col = 1; col <= numCols; col++) { - auto columnMeta = columnNames[col - 1].cast(); - SQLSMALLINT dataType = columnMeta["DataType"].cast(); - SQLULEN columnSize = columnMeta["ColumnSize"].cast(); + auto columnMeta = + columnNames[col - 1].cast(); + SQLSMALLINT dataType = + columnMeta["DataType"].cast(); + SQLULEN columnSize = + columnMeta["ColumnSize"].cast(); switch (dataType) { case SQL_CHAR: case SQL_VARCHAR: case SQL_LONGVARCHAR: { - // TODO: handle variable length data correctly. This logic wont suffice + // TODO: handle variable length data correctly. + // This logic wont suffice HandleZeroColumnSizeAtFetch(columnSize); - uint64_t fetchBufferSize = columnSize + 1 /*null-terminator*/; - // TODO: For LONGVARCHAR/BINARY types, columnSize is returned as 2GB-1 by - // SQLDescribeCol. So fetchBufferSize = 2GB. fetchSize=1 if columnSize>1GB. - // So we'll allocate a vector of size 2GB. If a query fetches multiple (say N) - // LONG... columns, we will have allocated multiple (N) 2GB sized vectors. This - // will make driver very slow. And if the N is high enough, we could hit the OS - // limit for heap memory that we can allocate, & hence get a std::bad_alloc. The - // process could also be killed by OS for consuming too much memory. - // Hence this will be revisited in beta to not allocate 2GB+ memory, - // & use streaming instead + // null-terminator + uint64_t fetchBufferSize = columnSize + 1; + // TODO: For LONGVARCHAR/BINARY types, columnSize is returned as 2GB-1 by + // SQLDescribeCol. So fetchBufferSize = 2GB. fetchSize=1 if columnSize>1GB. + // So we'll allocate a vector of size 2GB. + // If a query fetches multiple (say N) + // LONG... columns, we will have allocated multiple (N) 2GB sized vectors. + // This will make driver very slow. And if the N is high enough, + // we could hit the OS + // limit for heap memory that we can allocate, & hence get a std::bad_alloc. + // The process could also be killed by OS for consuming too much memory. + // Hence this will be revisited in beta to not allocate 2GB+ memory, + // & use streaming instead buffers.charBuffers[col - 1].resize(fetchSize * fetchBufferSize); - ret = SQLBindCol_ptr(hStmt, col, SQL_C_CHAR, buffers.charBuffers[col - 1].data(), - fetchBufferSize * sizeof(SQLCHAR), - buffers.indicators[col - 1].data()); + ret = SQLBindCol_ptr( + hStmt, col, SQL_C_CHAR, + buffers.charBuffers[col - 1].data(), + fetchBufferSize * sizeof(SQLCHAR), + buffers.indicators[col - 1].data()); break; } case SQL_WCHAR: case SQL_WVARCHAR: case SQL_WLONGVARCHAR: { - // TODO: handle variable length data correctly. This logic wont suffice + // TODO: handle variable length data correctly. + // This logic wont suffice HandleZeroColumnSizeAtFetch(columnSize); - uint64_t fetchBufferSize = columnSize + 1 /*null-terminator*/; + // null-terminator + uint64_t fetchBufferSize = columnSize + 1; buffers.wcharBuffers[col - 1].resize(fetchSize * fetchBufferSize); - ret = SQLBindCol_ptr(hStmt, col, SQL_C_WCHAR, buffers.wcharBuffers[col - 1].data(), - fetchBufferSize * sizeof(SQLWCHAR), - buffers.indicators[col - 1].data()); + ret = SQLBindCol_ptr( + hStmt, col, SQL_C_WCHAR, + buffers.wcharBuffers[col - 1].data(), + fetchBufferSize * sizeof(SQLWCHAR), + buffers.indicators[col - 1].data()); break; } case SQL_INTEGER: buffers.intBuffers[col - 1].resize(fetchSize); - ret = SQLBindCol_ptr(hStmt, col, SQL_C_SLONG, buffers.intBuffers[col - 1].data(), - sizeof(SQLINTEGER), buffers.indicators[col - 1].data()); + ret = SQLBindCol_ptr( + hStmt, col, SQL_C_SLONG, + buffers.intBuffers[col - 1].data(), + sizeof(SQLINTEGER), + buffers.indicators[col - 1].data()); break; case SQL_SMALLINT: buffers.smallIntBuffers[col - 1].resize(fetchSize); - ret = SQLBindCol_ptr(hStmt, col, SQL_C_SSHORT, - buffers.smallIntBuffers[col - 1].data(), sizeof(SQLSMALLINT), - buffers.indicators[col - 1].data()); + ret = SQLBindCol_ptr( + hStmt, col, SQL_C_SSHORT, + buffers.smallIntBuffers[col - 1].data(), + sizeof(SQLSMALLINT), + buffers.indicators[col - 1].data()); break; case SQL_TINYINT: buffers.charBuffers[col - 1].resize(fetchSize); - ret = SQLBindCol_ptr(hStmt, col, SQL_C_TINYINT, buffers.charBuffers[col - 1].data(), - sizeof(SQLCHAR), buffers.indicators[col - 1].data()); + ret = SQLBindCol_ptr( + hStmt, col, SQL_C_TINYINT, + buffers.charBuffers[col - 1].data(), + sizeof(SQLCHAR), + buffers.indicators[col - 1].data()); break; case SQL_BIT: buffers.charBuffers[col - 1].resize(fetchSize); - ret = SQLBindCol_ptr(hStmt, col, SQL_C_BIT, buffers.charBuffers[col - 1].data(), - sizeof(SQLCHAR), buffers.indicators[col - 1].data()); + ret = SQLBindCol_ptr( + hStmt, col, SQL_C_BIT, + buffers.charBuffers[col - 1].data(), + sizeof(SQLCHAR), + buffers.indicators[col - 1].data()); break; case SQL_REAL: buffers.realBuffers[col - 1].resize(fetchSize); - ret = SQLBindCol_ptr(hStmt, col, SQL_C_FLOAT, buffers.realBuffers[col - 1].data(), - sizeof(SQLREAL), buffers.indicators[col - 1].data()); + ret = SQLBindCol_ptr( + hStmt, col, SQL_C_FLOAT, + buffers.realBuffers[col - 1].data(), + sizeof(SQLREAL), + buffers.indicators[col - 1].data()); break; case SQL_DECIMAL: case SQL_NUMERIC: - buffers.charBuffers[col - 1].resize(fetchSize * MAX_DIGITS_IN_NUMERIC); - ret = SQLBindCol_ptr(hStmt, col, SQL_C_CHAR, buffers.charBuffers[col - 1].data(), - MAX_DIGITS_IN_NUMERIC * sizeof(SQLCHAR), - buffers.indicators[col - 1].data()); + buffers.charBuffers[col - 1].resize( + fetchSize * MAX_DIGITS_IN_NUMERIC); + ret = SQLBindCol_ptr( + hStmt, col, SQL_C_CHAR, + buffers.charBuffers[col - 1].data(), + MAX_DIGITS_IN_NUMERIC * sizeof(SQLCHAR), + buffers.indicators[col - 1].data()); break; case SQL_DOUBLE: case SQL_FLOAT: buffers.doubleBuffers[col - 1].resize(fetchSize); ret = - SQLBindCol_ptr(hStmt, col, SQL_C_DOUBLE, buffers.doubleBuffers[col - 1].data(), - sizeof(SQLDOUBLE), buffers.indicators[col - 1].data()); + SQLBindCol_ptr( + hStmt, col, SQL_C_DOUBLE, + buffers.doubleBuffers[col - 1].data(), + sizeof(SQLDOUBLE), + buffers.indicators[col - 1].data()); break; case SQL_TIMESTAMP: case SQL_TYPE_TIMESTAMP: case SQL_DATETIME: buffers.timestampBuffers[col - 1].resize(fetchSize); ret = SQLBindCol_ptr( - hStmt, col, SQL_C_TYPE_TIMESTAMP, buffers.timestampBuffers[col - 1].data(), - sizeof(SQL_TIMESTAMP_STRUCT), buffers.indicators[col - 1].data()); + hStmt, col, SQL_C_TYPE_TIMESTAMP, + buffers.timestampBuffers[col - 1].data(), + sizeof(SQL_TIMESTAMP_STRUCT), + buffers.indicators[col - 1].data()); break; case SQL_BIGINT: buffers.bigIntBuffers[col - 1].resize(fetchSize); ret = - SQLBindCol_ptr(hStmt, col, SQL_C_SBIGINT, buffers.bigIntBuffers[col - 1].data(), - sizeof(SQLBIGINT), buffers.indicators[col - 1].data()); + SQLBindCol_ptr( + hStmt, col, SQL_C_SBIGINT, + buffers.bigIntBuffers[col - 1].data(), + sizeof(SQLBIGINT), + buffers.indicators[col - 1].data()); break; case SQL_TYPE_DATE: buffers.dateBuffers[col - 1].resize(fetchSize); ret = - SQLBindCol_ptr(hStmt, col, SQL_C_TYPE_DATE, buffers.dateBuffers[col - 1].data(), - sizeof(SQL_DATE_STRUCT), buffers.indicators[col - 1].data()); + SQLBindCol_ptr( + hStmt, col, SQL_C_TYPE_DATE, + buffers.dateBuffers[col - 1].data(), + sizeof(SQL_DATE_STRUCT), + buffers.indicators[col - 1].data()); break; case SQL_TIME: case SQL_TYPE_TIME: case SQL_SS_TIME2: buffers.timeBuffers[col - 1].resize(fetchSize); ret = - SQLBindCol_ptr(hStmt, col, SQL_C_TYPE_TIME, buffers.timeBuffers[col - 1].data(), - sizeof(SQL_TIME_STRUCT), buffers.indicators[col - 1].data()); + SQLBindCol_ptr( + hStmt, col, SQL_C_TYPE_TIME, + buffers.timeBuffers[col - 1].data(), + sizeof(SQL_TIME_STRUCT), + buffers.indicators[col - 1].data()); break; case SQL_GUID: buffers.guidBuffers[col - 1].resize(fetchSize); - ret = SQLBindCol_ptr(hStmt, col, SQL_C_GUID, buffers.guidBuffers[col - 1].data(), - sizeof(SQLGUID), buffers.indicators[col - 1].data()); + ret = SQLBindCol_ptr( + hStmt, col, SQL_C_GUID, + buffers.guidBuffers[col - 1].data(), + sizeof(SQLGUID), + buffers.indicators[col - 1].data()); break; case SQL_BINARY: case SQL_VARBINARY: case SQL_LONGVARBINARY: - // TODO: handle variable length data correctly. This logic wont suffice + // TODO: handle variable length data correctly. + // This logic wont suffice HandleZeroColumnSizeAtFetch(columnSize); buffers.charBuffers[col - 1].resize(fetchSize * columnSize); - ret = SQLBindCol_ptr(hStmt, col, SQL_C_BINARY, buffers.charBuffers[col - 1].data(), - columnSize, buffers.indicators[col - 1].data()); + ret = SQLBindCol_ptr( + hStmt, col, SQL_C_BINARY, + buffers.charBuffers[col - 1].data(), + columnSize, + buffers.indicators[col - 1].data()); break; default: - std::wstring columnName = columnMeta["ColumnName"].cast(); + std::wstring columnName = + columnMeta["ColumnName"].cast(); std::ostringstream errorString; - errorString << "Unsupported data type for column - " << columnName.c_str() - << ", Type - " << dataType << ", column ID - " << col; + errorString << "Unsupported data type for column - " + << columnName.c_str() + << ", Type - " << dataType + << ", column ID - " << col; LOG(errorString.str()); ThrowStdException(errorString.str()); break; } if (!SQL_SUCCEEDED(ret)) { - std::wstring columnName = columnMeta["ColumnName"].cast(); + std::wstring columnName = + columnMeta["ColumnName"].cast(); std::ostringstream errorString; - errorString << "Failed to bind column - " << columnName.c_str() << ", Type - " - << dataType << ", column ID - " << col; + errorString << "Failed to bind column - " + << columnName.c_str() + << ", Type - " << dataType + << ", column ID - " << col; LOG(errorString.str()); ThrowStdException(errorString.str()); return ret; @@ -2285,8 +2910,11 @@ SQLRETURN SQLBindColums(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& column // Fetch rows in batches // TODO: Move to anonymous namespace, since it is not used outside this file -SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& columnNames, - py::list& rows, SQLUSMALLINT numCols, SQLULEN& numRowsFetched) { +SQLRETURN FetchBatchData( + SQLHSTMT hStmt, ColumnBuffers& buffers, + py::list& columnNames, + py::list& rows, SQLUSMALLINT numCols, + SQLULEN& numRowsFetched) { LOG("Fetching data in batches"); SQLRETURN ret = SQLFetchScroll_ptr(hStmt, SQL_FETCH_NEXT, 0); if (ret == SQL_NO_DATA) { @@ -2297,50 +2925,69 @@ SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& colum LOG("Error while fetching rows in batches"); return ret; } - // numRowsFetched is the SQL_ATTR_ROWS_FETCHED_PTR attribute. It'll be populated by - // SQLFetchScroll + // numRowsFetched is the SQL_ATTR_ROWS_FETCHED_PTR attribute. + // It'll be populated by SQLFetchScroll for (SQLULEN i = 0; i < numRowsFetched; i++) { py::list row; for (SQLUSMALLINT col = 1; col <= numCols; col++) { - auto columnMeta = columnNames[col - 1].cast(); - SQLSMALLINT dataType = columnMeta["DataType"].cast(); - SQLLEN dataLen = buffers.indicators[col - 1][i]; + auto columnMeta = + columnNames[col - 1].cast(); + SQLSMALLINT dataType = + columnMeta["DataType"].cast(); + SQLLEN dataLen = + buffers.indicators[col - 1][i]; if (dataLen == SQL_NULL_DATA) { row.append(py::none()); continue; } - // TODO: variable length data needs special handling, this logic wont suffice - // This value indicates that the driver cannot determine the length of the data + // TODO: variable length data needs special handling, + // this logic wont suffice + // This value indicates that the driver + // cannot determine the length of the data if (dataLen == SQL_NO_TOTAL) { - LOG("Cannot determine the length of the data. Returning NULL value instead." + LOG("Cannot determine the length of the data. " + "Returning NULL value instead." "Column ID - {}", col); row.append(py::none()); continue; } - assert(dataLen > 0 && "Must be > 0 since SQL_NULL_DATA & SQL_NO_DATA is already handled"); + assert( + dataLen > 0 && + "Must be > 0 since SQL_NULL_DATA & " + "SQL_NO_DATA is already handled"); switch (dataType) { case SQL_CHAR: case SQL_VARCHAR: case SQL_LONGVARCHAR: { - // TODO: variable length data needs special handling, this logic wont suffice - SQLULEN columnSize = columnMeta["ColumnSize"].cast(); + // TODO: variable length data needs special handling, + // this logic wont suffice + SQLULEN columnSize = + columnMeta["ColumnSize"].cast(); HandleZeroColumnSizeAtFetch(columnSize); - uint64_t fetchBufferSize = columnSize + 1 /*null-terminator*/; - uint64_t numCharsInData = dataLen / sizeof(SQLCHAR); - // fetchBufferSize includes null-terminator, numCharsInData doesn't. Hence '<' + // null-terminator + uint64_t fetchBufferSize = columnSize + 1; + uint64_t numCharsInData = dataLen / sizeof(SQLCHAR); + // fetchBufferSize includes null-terminator, + // numCharsInData doesn't. Hence '<' if (numCharsInData < fetchBufferSize) { // SQLFetch will nullterminate the data row.append(std::string( - reinterpret_cast(&buffers.charBuffers[col - 1][i * fetchBufferSize]), - numCharsInData)); + reinterpret_cast( + &buffers.charBuffers[col - 1][i * fetchBufferSize]), + numCharsInData)); } else { - // In this case, buffer size is smaller, and data to be retrieved is longer + // In this case, buffer size is smaller, + // and data to be retrieved is longer // TODO: Revisit std::ostringstream oss; - oss << "Buffer length for fetch (" << columnSize << ") is smaller, & data " - << "to be retrieved is longer (" << numCharsInData << "). ColumnID - " + oss << "Buffer length for fetch (" + << columnSize << + ") is smaller, & data " + << "to be retrieved is longer (" + << numCharsInData << + "). ColumnID - " << col << ", datatype - " << dataType; ThrowStdException(oss.str()); } @@ -2349,50 +2996,68 @@ SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& colum case SQL_WCHAR: case SQL_WVARCHAR: case SQL_WLONGVARCHAR: { - // TODO: variable length data needs special handling, this logic wont suffice - SQLULEN columnSize = columnMeta["ColumnSize"].cast(); + // TODO: variable length data needs special + // handling, this logic wont suffice + SQLULEN columnSize = + columnMeta["ColumnSize"].cast(); HandleZeroColumnSizeAtFetch(columnSize); - uint64_t fetchBufferSize = columnSize + 1 /*null-terminator*/; - uint64_t numCharsInData = dataLen / sizeof(SQLWCHAR); - // fetchBufferSize includes null-terminator, numCharsInData doesn't. Hence '<' + // null-terminator + uint64_t fetchBufferSize = columnSize + 1; + uint64_t numCharsInData = dataLen / sizeof(SQLWCHAR); + // fetchBufferSize includes null-terminator, + // numCharsInData doesn't. Hence '<' if (numCharsInData < fetchBufferSize) { // SQLFetch will nullterminate the data #if defined(__APPLE__) || defined(__linux__) - // Use unix-specific conversion to handle the wchar_t/SQLWCHAR size difference - SQLWCHAR* wcharData = &buffers.wcharBuffers[col - 1][i * fetchBufferSize]; - std::wstring wstr = SQLWCHARToWString(wcharData, numCharsInData); + // Use unix-specific conversion to handle + // the wchar_t/SQLWCHAR size difference + SQLWCHAR* wcharData = + &buffers.wcharBuffers[col - 1][i * fetchBufferSize]; + std::wstring wstr = + SQLWCHARToWString(wcharData, numCharsInData); row.append(wstr); #else - // On Windows, wchar_t and SQLWCHAR are both 2 bytes, so direct cast works + // On Windows, wchar_t and SQLWCHAR are both 2 bytes, + // so direct cast works row.append(std::wstring( - reinterpret_cast(&buffers.wcharBuffers[col - 1][i * fetchBufferSize]), + reinterpret_cast( + &buffers.wcharBuffers[col - 1][i * fetchBufferSize]), numCharsInData)); #endif } else { - // In this case, buffer size is smaller, and data to be retrieved is longer + // In this case, buffer size is smaller, + // and data to be retrieved is longer // TODO: Revisit std::ostringstream oss; - oss << "Buffer length for fetch (" << columnSize << ") is smaller, & data " - << "to be retrieved is longer (" << numCharsInData << "). ColumnID - " + oss << "Buffer length for fetch (" + << columnSize << + ") is smaller, & data " + << "to be retrieved is longer (" + << numCharsInData << + "). ColumnID - " << col << ", datatype - " << dataType; ThrowStdException(oss.str()); } break; } case SQL_INTEGER: { - row.append(buffers.intBuffers[col - 1][i]); + row.append( + buffers.intBuffers[col - 1][i]); break; } case SQL_SMALLINT: { - row.append(buffers.smallIntBuffers[col - 1][i]); + row.append( + buffers.smallIntBuffers[col - 1][i]); break; } case SQL_TINYINT: { - row.append(buffers.charBuffers[col - 1][i]); + row.append( + buffers.charBuffers[col - 1][i]); break; } case SQL_BIT: { - row.append(static_cast(buffers.charBuffers[col - 1][i])); + row.append(static_cast( + buffers.charBuffers[col - 1][i])); break; } case SQL_REAL: { @@ -2402,13 +3067,17 @@ SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& colum case SQL_DECIMAL: case SQL_NUMERIC: { try { - // Convert numericStr to py::decimal.Decimal and append to row - row.append(py::module_::import("decimal").attr("Decimal")(std::string( - reinterpret_cast( - &buffers.charBuffers[col - 1][i * MAX_DIGITS_IN_NUMERIC]), - buffers.indicators[col - 1][i]))); + // Convert numericStr to py::decimal. + // Decimal and append to row + row.append( + py::module_::import("decimal").attr("Decimal")( + std::string( + reinterpret_cast( + &buffers.charBuffers[col - 1][i * MAX_DIGITS_IN_NUMERIC]), + buffers.indicators[col - 1][i]))); } catch (const py::error_already_set& e) { - // Handle the exception, e.g., log the error and append py::none() + // Handle the exception, e.g., + // log the error and append py::none() LOG("Error converting to decimal: {}", e.what()); row.append(py::none()); } @@ -2423,13 +3092,16 @@ SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& colum case SQL_TYPE_TIMESTAMP: case SQL_DATETIME: { row.append(py::module_::import("datetime") - .attr("datetime")(buffers.timestampBuffers[col - 1][i].year, - buffers.timestampBuffers[col - 1][i].month, - buffers.timestampBuffers[col - 1][i].day, - buffers.timestampBuffers[col - 1][i].hour, - buffers.timestampBuffers[col - 1][i].minute, - buffers.timestampBuffers[col - 1][i].second, - buffers.timestampBuffers[col - 1][i].fraction / 1000 /* Convert back ns to µs */)); + .attr("datetime")( + buffers.timestampBuffers[col - 1][i].year, + buffers.timestampBuffers[col - 1][i].month, + buffers.timestampBuffers[col - 1][i].day, + buffers.timestampBuffers[col - 1][i].hour, + buffers.timestampBuffers[col - 1][i].minute, + buffers.timestampBuffers[col - 1][i].second, + // Convert back ns to µs + buffers.timestampBuffers[col - 1][i].fraction + / 1000)); break; } case SQL_BIGINT: { @@ -2438,52 +3110,62 @@ SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& colum } case SQL_TYPE_DATE: { row.append(py::module_::import("datetime") - .attr("date")(buffers.dateBuffers[col - 1][i].year, - buffers.dateBuffers[col - 1][i].month, - buffers.dateBuffers[col - 1][i].day)); + .attr("date")( + buffers.dateBuffers[col - 1][i].year, + buffers.dateBuffers[col - 1][i].month, + buffers.dateBuffers[col - 1][i].day)); break; } case SQL_TIME: case SQL_TYPE_TIME: case SQL_SS_TIME2: { row.append(py::module_::import("datetime") - .attr("time")(buffers.timeBuffers[col - 1][i].hour, - buffers.timeBuffers[col - 1][i].minute, - buffers.timeBuffers[col - 1][i].second)); + .attr("time")( + buffers.timeBuffers[col - 1][i].hour, + buffers.timeBuffers[col - 1][i].minute, + buffers.timeBuffers[col - 1][i].second)); break; } case SQL_GUID: { row.append( - py::bytes(reinterpret_cast(&buffers.guidBuffers[col - 1][i]), + py::bytes(reinterpret_cast( + &buffers.guidBuffers[col - 1][i]), sizeof(SQLGUID))); break; } case SQL_BINARY: case SQL_VARBINARY: case SQL_LONGVARBINARY: { - // TODO: variable length data needs special handling, this logic wont suffice - SQLULEN columnSize = columnMeta["ColumnSize"].cast(); + // TODO: variable length data needs special handling, + // this logic wont suffice + SQLULEN columnSize = + columnMeta["ColumnSize"].cast(); HandleZeroColumnSizeAtFetch(columnSize); if (static_cast(dataLen) <= columnSize) { row.append(py::bytes(reinterpret_cast( - &buffers.charBuffers[col - 1][i * columnSize]), - dataLen)); + &buffers.charBuffers[col - 1][i * columnSize]), + dataLen)); } else { - // In this case, buffer size is smaller, and data to be retrieved is longer + // In this case, buffer size is smaller, + // and data to be retrieved is longer // TODO: Revisit std::ostringstream oss; - oss << "Buffer length for fetch (" << columnSize << ") is smaller, & data " - << "to be retrieved is longer (" << dataLen << "). ColumnID - " - << col << ", datatype - " << dataType; + oss << "Buffer length for fetch (" << columnSize << + ") is smaller, & data " << + "to be retrieved is longer (" + << dataLen << "). ColumnID - " << col << + ", datatype - " << dataType; ThrowStdException(oss.str()); } break; } default: { - std::wstring columnName = columnMeta["ColumnName"].cast(); + std::wstring columnName = + columnMeta["ColumnName"].cast(); std::ostringstream errorString; - errorString << "Unsupported data type for column - " << columnName.c_str() - << ", Type - " << dataType << ", column ID - " << col; + errorString << "Unsupported data type for column - " << + columnName.c_str() << ", Type - " << + dataType << ", column ID - " << col; LOG(errorString.str()); ThrowStdException(errorString.str()); break; @@ -2495,7 +3177,8 @@ SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& colum return ret; } -// Given a list of columns that are a part of single row in the result set, calculates +// Given a list of columns that are a part of single +// row in the result set, calculates // the max size of the row // TODO: Move to anonymous namespace, since it is not used outside this file size_t calculateRowSize(py::list& columnNames, SQLUSMALLINT numCols) { @@ -2564,10 +3247,12 @@ size_t calculateRowSize(py::list& columnNames, SQLUSMALLINT numCols) { rowSize += columnSize; break; default: - std::wstring columnName = columnMeta["ColumnName"].cast(); + std::wstring columnName = + columnMeta["ColumnName"].cast(); std::ostringstream errorString; - errorString << "Unsupported data type for column - " << columnName.c_str() - << ", Type - " << dataType << ", column ID - " << col; + errorString << "Unsupported data type for column - " << + columnName.c_str() << ", Type - " << dataType << + ", column ID - " << col; LOG(errorString.str()); ThrowStdException(errorString.str()); break; @@ -2578,19 +3263,25 @@ size_t calculateRowSize(py::list& columnNames, SQLUSMALLINT numCols) { // FetchMany_wrap - Fetches multiple rows of data from the result set. // -// @param StatementHandle: Handle to the statement from which data is to be fetched. -// @param rows: A Python list that will be populated with the fetched rows of data. +// @param StatementHandle: Handle to the statement from +// which data is to be fetched. +// @param rows: A Python list that will be populated +// with the fetched rows of data. // @param fetchSize: The number of rows to fetch. Default value is 1. // // @return SQLRETURN: SQL_SUCCESS if data is fetched successfully, // SQL_NO_DATA if there are no more rows to fetch, // throws a runtime error if there is an error fetching data. // -// This function assumes that the statement handle (hStmt) is already allocated and a query has been -// executed. It fetches the specified number of rows from the result set and populates the provided -// Python list with the row data. If there are no more rows to fetch, it returns SQL_NO_DATA. If an -// error occurs during fetching, it throws a runtime error. -SQLRETURN FetchMany_wrap(SqlHandlePtr StatementHandle, py::list& rows, int fetchSize = 1) { +// This function assumes that the statement handle (hStmt) +// is already allocated and a query has been +// executed. It fetches the specified number of rows +// from the result set and populates the provided +// Python list with the row data. +// If there are no more rows to fetch, it returns SQL_NO_DATA. +// If an error occurs during fetching, it throws a runtime error. +SQLRETURN FetchMany_wrap(SqlHandlePtr StatementHandle, + py::list& rows, int fetchSize = 1) { SQLRETURN ret; SQLHSTMT hStmt = StatementHandle->get(); // Retrieve column count @@ -2615,10 +3306,12 @@ SQLRETURN FetchMany_wrap(SqlHandlePtr StatementHandle, py::list& rows, int fetch } SQLULEN numRowsFetched; - SQLSetStmtAttr_ptr(hStmt, SQL_ATTR_ROW_ARRAY_SIZE, (SQLPOINTER)(intptr_t)fetchSize, 0); + SQLSetStmtAttr_ptr(hStmt, SQL_ATTR_ROW_ARRAY_SIZE, + (SQLPOINTER)(intptr_t)fetchSize, 0); SQLSetStmtAttr_ptr(hStmt, SQL_ATTR_ROWS_FETCHED_PTR, &numRowsFetched, 0); - ret = FetchBatchData(hStmt, buffers, columnNames, rows, numCols, numRowsFetched); + ret = FetchBatchData(hStmt, buffers, columnNames, + rows, numCols, numRowsFetched); if (!SQL_SUCCEEDED(ret) && ret != SQL_NO_DATA) { LOG("Error when fetching data"); return ret; @@ -2633,16 +3326,21 @@ SQLRETURN FetchMany_wrap(SqlHandlePtr StatementHandle, py::list& rows, int fetch // FetchAll_wrap - Fetches all rows of data from the result set. // -// @param StatementHandle: Handle to the statement from which data is to be fetched. -// @param rows: A Python list that will be populated with the fetched rows of data. +// @param StatementHandle: Handle to the statement +// from which data is to be fetched. +// @param rows: A Python list that will be +// populated with the fetched rows of data. // // @return SQLRETURN: SQL_SUCCESS if data is fetched successfully, // SQL_NO_DATA if there are no more rows to fetch, // throws a runtime error if there is an error fetching data. // -// This function assumes that the statement handle (hStmt) is already allocated and a query has been -// executed. It fetches all rows from the result set and populates the provided Python list with the -// row data. If there are no more rows to fetch, it returns SQL_NO_DATA. If an error occurs during +// This function assumes that the statement handle (hStmt) is +// already allocated and a query has been +// executed. It fetches all rows from the result set and +// populates the provided Python list with the +// row data. If there are no more rows to fetch, +// it returns SQL_NO_DATA. If an error occurs during // fetching, it throws a runtime error. SQLRETURN FetchAll_wrap(SqlHandlePtr StatementHandle, py::list& rows) { SQLRETURN ret; @@ -2676,15 +3374,18 @@ SQLRETURN FetchAll_wrap(SqlHandlePtr StatementHandle, py::list& rows) { // into account. So, we will end up fetching 1000 rows at a time. numRowsInMemLimit = 1; // fetchsize will be 10 } - // TODO: Revisit this logic. Eventhough we're fetching fetchSize rows at a time, - // fetchall will keep all rows in memory anyway. So what are we gaining by fetching - // fetchSize rows at a time? - // Also, say the table has only 10 rows, each row size if 100 bytes. Here, we'll have - // fetchSize = 1000, so we'll allocate memory for 1000 rows inside SQLBindCol_wrap, while - // actually only need to retrieve 10 rows + // TODO: Revisit this logic. + // Even though we're fetching fetchSize rows at a time, + // fetchall will keep all rows in memory anyway. + // So what are we gaining by fetching fetchSize rows at a time? + // Also, say the table has only 10 rows, each row size if 100 bytes. + // Here, we'll have fetchSize = 1000, so we'll allocate memory for + // 1000 rows inside SQLBindCol_wrap, while actually only need to + // retrieve 10 rows int fetchSize; if (numRowsInMemLimit == 0) { - // If the row size is larger than the memory limit, fetch one row at a time + // If the row size is larger than the memory limit, + // fetch one row at a time fetchSize = 1; } else if (numRowsInMemLimit > 0 && numRowsInMemLimit <= 100) { // If between 1-100 rows fit in memoryLimit, fetch 10 rows at a time @@ -2707,17 +3408,19 @@ SQLRETURN FetchAll_wrap(SqlHandlePtr StatementHandle, py::list& rows) { } SQLULEN numRowsFetched; - SQLSetStmtAttr_ptr(hStmt, SQL_ATTR_ROW_ARRAY_SIZE, (SQLPOINTER)(intptr_t)fetchSize, 0); + SQLSetStmtAttr_ptr(hStmt, SQL_ATTR_ROW_ARRAY_SIZE, + (SQLPOINTER)(intptr_t)fetchSize, 0); SQLSetStmtAttr_ptr(hStmt, SQL_ATTR_ROWS_FETCHED_PTR, &numRowsFetched, 0); while (ret != SQL_NO_DATA) { - ret = FetchBatchData(hStmt, buffers, columnNames, rows, numCols, numRowsFetched); + ret = FetchBatchData(hStmt, buffers, + columnNames, rows, numCols, numRowsFetched); if (!SQL_SUCCEEDED(ret) && ret != SQL_NO_DATA) { LOG("Error when fetching data"); return ret; } } - + // Reset attributes before returning to avoid using stack pointers later SQLSetStmtAttr_ptr(hStmt, SQL_ATTR_ROW_ARRAY_SIZE, (SQLPOINTER)1, 0); SQLSetStmtAttr_ptr(hStmt, SQL_ATTR_ROWS_FETCHED_PTR, NULL, 0); @@ -2727,16 +3430,21 @@ SQLRETURN FetchAll_wrap(SqlHandlePtr StatementHandle, py::list& rows) { // FetchOne_wrap - Fetches a single row of data from the result set. // -// @param StatementHandle: Handle to the statement from which data is to be fetched. +// @param StatementHandle: Handle to the statement +// from which data is to be fetched. // @param row: A Python list that will be populated with the fetched row data. // -// @return SQLRETURN: SQL_SUCCESS or SQL_SUCCESS_WITH_INFO if data is fetched successfully, +// @return SQLRETURN: SQL_SUCCESS or SQL_SUCCESS_WITH_INFO +// if data is fetched successfully, // SQL_NO_DATA if there are no more rows to fetch, // throws a runtime error if there is an error fetching data. // -// This function assumes that the statement handle (hStmt) is already allocated and a query has been -// executed. It fetches the next row of data from the result set and populates the provided Python -// list with the row data. If there are no more rows to fetch, it returns SQL_NO_DATA. If an error +// This function assumes that the statement handle (hStmt) +// is already allocated and a query has been +// executed. It fetches the next row of data from the result +// set and populates the provided Python +// list with the row data. If there are no more rows to fetch, +// it returns SQL_NO_DATA. If an error // occurs during fetching, it throws a runtime error. SQLRETURN FetchOne_wrap(SqlHandlePtr StatementHandle, py::list& row) { SQLRETURN ret; @@ -2808,7 +3516,8 @@ void enable_pooling(int maxSize, int idleTimeout) { // Architecture-specific defines #ifndef ARCHITECTURE -#define ARCHITECTURE "win64" // Default to win64 if not defined during compilation +// Default to win64 if not defined during compilation +#define ARCHITECTURE "win64" #endif // Functions/data to be exposed to Python as a part of ddbc_bindings module @@ -2820,10 +3529,11 @@ PYBIND11_MODULE(ddbc_bindings, m) { // Expose architecture-specific constants m.attr("ARCHITECTURE") = ARCHITECTURE; - + // Expose the C++ functions to Python m.def("ThrowStdException", &ThrowStdException); - m.def("GetDriverPathCpp", &GetDriverPathCpp, "Get the path to the ODBC driver"); + m.def("GetDriverPathCpp", &GetDriverPathCpp, + "Get the path to the ODBC driver"); // Define parameter info class py::class_(m, "ParamInfo") @@ -2850,58 +3560,81 @@ PYBIND11_MODULE(ddbc_bindings, m) { py::class_(m, "ErrorInfo") .def_readwrite("sqlState", &ErrorInfo::sqlState) .def_readwrite("ddbcErrorMsg", &ErrorInfo::ddbcErrorMsg); - + py::class_(m, "SqlHandle") .def("free", &SqlHandle::free, "Free the handle"); - + py::class_(m, "Connection") - .def(py::init(), py::arg("conn_str"), py::arg("use_pool"), py::arg("attrs_before") = py::dict()) - .def("close", &ConnectionHandle::close, "Close the connection") - .def("commit", &ConnectionHandle::commit, "Commit the current transaction") - .def("rollback", &ConnectionHandle::rollback, "Rollback the current transaction") + .def( + py::init(), + py::arg("conn_str"), + py::arg("use_pool"), + py::arg("attrs_before") = py::dict() + ) + .def("close", &ConnectionHandle::close, + "Close the connection") + .def("commit", &ConnectionHandle::commit, + "Commit the current transaction") + .def("rollback", &ConnectionHandle::rollback, + "Rollback the current transaction") .def("set_autocommit", &ConnectionHandle::setAutocommit) .def("get_autocommit", &ConnectionHandle::getAutocommit) .def("alloc_statement_handle", &ConnectionHandle::allocStatementHandle); - m.def("enable_pooling", &enable_pooling, "Enable global connection pooling"); - m.def("close_pooling", []() {ConnectionPoolManager::getInstance().closePools();}); - m.def("DDBCSQLExecDirect", &SQLExecDirect_wrap, "Execute a SQL query directly"); - m.def("DDBCSQLExecute", &SQLExecute_wrap, "Prepare and execute T-SQL statements"); - m.def("SQLExecuteMany", &SQLExecuteMany_wrap, "Execute statement with multiple parameter sets"); + m.def("enable_pooling", &enable_pooling, + "Enable global connection pooling"); + m.def("close_pooling", + []() {ConnectionPoolManager::getInstance().closePools();}); + m.def("DDBCSQLExecDirect", &SQLExecDirect_wrap, + "Execute a SQL query directly"); + m.def("DDBCSQLExecute", &SQLExecute_wrap, + "Prepare and execute T-SQL statements"); + m.def("SQLExecuteMany", &SQLExecuteMany_wrap, + "Execute statement with multiple parameter sets"); m.def("DDBCSQLRowCount", &SQLRowCount_wrap, "Get the number of rows affected by the last statement"); - m.def("DDBCSQLFetch", &SQLFetch_wrap, "Fetch the next row from the result set"); + m.def("DDBCSQLFetch", &SQLFetch_wrap, + "Fetch the next row from the result set"); m.def("DDBCSQLNumResultCols", &SQLNumResultCols_wrap, "Get the number of columns in the result set"); m.def("DDBCSQLDescribeCol", &SQLDescribeCol_wrap, "Get information about a column in the result set"); - m.def("DDBCSQLGetData", &SQLGetData_wrap, "Retrieve data from the result set"); - m.def("DDBCSQLMoreResults", &SQLMoreResults_wrap, "Check for more results in the result set"); - m.def("DDBCSQLFetchOne", &FetchOne_wrap, "Fetch one row from the result set"); - m.def("DDBCSQLFetchMany", &FetchMany_wrap, py::arg("StatementHandle"), py::arg("rows"), + m.def("DDBCSQLGetData", &SQLGetData_wrap, + "Retrieve data from the result set"); + m.def("DDBCSQLMoreResults", &SQLMoreResults_wrap, + "Check for more results in the result set"); + m.def("DDBCSQLFetchOne", &FetchOne_wrap, + "Fetch one row from the result set"); + m.def("DDBCSQLFetchMany", &FetchMany_wrap, + py::arg("StatementHandle"), py::arg("rows"), py::arg("fetchSize") = 1, "Fetch many rows from the result set"); - m.def("DDBCSQLFetchAll", &FetchAll_wrap, "Fetch all rows from the result set"); + m.def("DDBCSQLFetchAll", &FetchAll_wrap, + "Fetch all rows from the result set"); m.def("DDBCSQLFreeHandle", &SQLFreeHandle_wrap, "Free a handle"); m.def("DDBCSQLCheckError", &SQLCheckError_Wrap, "Check for driver errors"); m.def("DDBCSQLGetAllDiagRecords", &SQLGetAllDiagRecords, "Get all diagnostic records for a handle", py::arg("handle")); - m.def("DDBCSQLTables", &SQLTables_wrap, + m.def("DDBCSQLTables", &SQLTables_wrap, "Get table information using ODBC SQLTables", - py::arg("StatementHandle"), py::arg("catalog") = std::wstring(), - py::arg("schema") = std::wstring(), py::arg("table") = std::wstring(), + py::arg("StatementHandle"), py::arg("catalog") = std::wstring(), + py::arg("schema") = std::wstring(), py::arg("table") = std::wstring(), py::arg("tableType") = std::wstring()); m.def("DDBCSQLFetchScroll", &SQLFetchScroll_wrap, - "Scroll to a specific position in the result set and optionally fetch data"); + "Scroll to a specific position in the result " + "set and optionally fetch data"); // Add a version attribute m.attr("__version__") = "1.0.0"; - + try { // Try loading the ODBC driver when the module is imported LOG("Loading ODBC driver"); DriverLoader::getInstance().loadDriver(); // Load the driver } catch (const std::exception& e) { - // Log the error but don't throw - let the error happen when functions are called - LOG("Failed to load ODBC driver during module initialization: {}", e.what()); + // Log the error but don't throw - + // let the error happen when functions are called + LOG( + "Failed to load ODBC driver during module initialization: {}", + e.what()); } } diff --git a/mssql_python/pybind/ddbc_bindings.h b/mssql_python/pybind/ddbc_bindings.h index 521a007b..f46bf42d 100644 --- a/mssql_python/pybind/ddbc_bindings.h +++ b/mssql_python/pybind/ddbc_bindings.h @@ -1,32 +1,33 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -// INFO|TODO - Note that is file is Windows specific right now. Making it arch agnostic will be -// taken up in future. +// INFO|TODO - Note that is file is Windows specific right now. Making it arch +// agnostic will be taken up in future. #pragma once -#include // pybind11.h must be the first include - https://pybind11.readthedocs.io/en/latest/basics.html#header-and-namespace-conventions #include #include #include -#include // Add this line for datetime support +#include // pybind11.h must be the first include - https://pybind11.readthedocs.io/en/latest/basics.html#header-and-namespace-conventions +#include // Add this line for datetime support #include namespace py = pybind11; -using namespace pybind11::literals; +using pybind11::literals::operator""_a; -#include #include #include +#include +#include #ifdef _WIN32 - // Windows-specific headers - #include // windows.h needs to be included before sql.h - #include - #pragma comment(lib, "shlwapi.lib") - #define IS_WINDOWS 1 +// Windows-specific headers +#include // windows.h needs to be included before sql.h +#include +#pragma comment(lib, "shlwapi.lib") +#define IS_WINDOWS 1 #else - #define IS_WINDOWS 0 +#define IS_WINDOWS 0 #endif #include @@ -41,24 +42,27 @@ inline std::vector WStringToSQLWCHAR(const std::wstring& str) { #endif #if defined(__APPLE__) || defined(__linux__) -#include +#include "pybind/unix_buffers.h" // For Unix-specific buffer handling +#include "pybind/unix_utils.h" // For Unix-specific Unicode encoding fixes // Unicode constants for surrogate ranges and max scalar value constexpr uint32_t UNICODE_SURROGATE_HIGH_START = 0xD800; -constexpr uint32_t UNICODE_SURROGATE_HIGH_END = 0xDBFF; -constexpr uint32_t UNICODE_SURROGATE_LOW_START = 0xDC00; -constexpr uint32_t UNICODE_SURROGATE_LOW_END = 0xDFFF; -constexpr uint32_t UNICODE_MAX_CODEPOINT = 0x10FFFF; -constexpr uint32_t UNICODE_REPLACEMENT_CHAR = 0xFFFD; +constexpr uint32_t UNICODE_SURROGATE_HIGH_END = 0xDBFF; +constexpr uint32_t UNICODE_SURROGATE_LOW_START = 0xDC00; +constexpr uint32_t UNICODE_SURROGATE_LOW_END = 0xDFFF; +constexpr uint32_t UNICODE_MAX_CODEPOINT = 0x10FFFF; +constexpr uint32_t UNICODE_REPLACEMENT_CHAR = 0xFFFD; // Validate whether a code point is a legal Unicode scalar value // (excludes surrogate halves and values beyond U+10FFFF) inline bool IsValidUnicodeScalar(uint32_t cp) { return cp <= UNICODE_MAX_CODEPOINT && - !(cp >= UNICODE_SURROGATE_HIGH_START && cp <= UNICODE_SURROGATE_LOW_END); + !(cp >= UNICODE_SURROGATE_HIGH_START && + cp <= UNICODE_SURROGATE_LOW_END); } -inline std::wstring SQLWCHARToWString(const SQLWCHAR* sqlwStr, size_t length = SQL_NTS) { +inline std::wstring SQLWCHARToWString(const SQLWCHAR* sqlwStr, + size_t length = SQL_NTS) { if (!sqlwStr) return std::wstring(); if (length == SQL_NTS) { @@ -74,39 +78,55 @@ inline std::wstring SQLWCHARToWString(const SQLWCHAR* sqlwStr, size_t length = S for (size_t i = 0; i < length; ++i) { uint16_t wc = static_cast(sqlwStr[i]); // Check if this is a high surrogate (U+D800–U+DBFF) - if (wc >= UNICODE_SURROGATE_HIGH_START && wc <= UNICODE_SURROGATE_HIGH_END && i + 1 < length) { - uint16_t low = static_cast(sqlwStr[i + 1]); - // Check if the next code unit is a low surrogate (U+DC00–U+DFFF) - if (low >= UNICODE_SURROGATE_LOW_START && low <= UNICODE_SURROGATE_LOW_END) { + if (wc >= UNICODE_SURROGATE_HIGH_START && + wc <= UNICODE_SURROGATE_HIGH_END && + i + 1 < length) { + uint16_t low = + static_cast(sqlwStr[i + 1]); + // Check if the next code unit is a low surrogate + // (U+DC00–U+DFFF) + if (low >= UNICODE_SURROGATE_LOW_START && + low <= UNICODE_SURROGATE_LOW_END) { // Combine surrogate pair into a single code point - uint32_t cp = (((wc - UNICODE_SURROGATE_HIGH_START) << 10) | (low - UNICODE_SURROGATE_LOW_START)) + 0x10000; - result.push_back(static_cast(cp)); - ++i; // Skip the low surrogate + uint32_t cp = + (((wc - UNICODE_SURROGATE_HIGH_START) << 10) | + (low - UNICODE_SURROGATE_LOW_START)) + + 0x10000; + result.push_back( + static_cast(cp)); + ++i; // Skip the low surrogate continue; } } - // If valid scalar then append, else append replacement char (U+FFFD) + // If valid scalar then append, else append replacement char + // (U+FFFD) if (IsValidUnicodeScalar(wc)) { - result.push_back(static_cast(wc)); + result.push_back( + static_cast(wc)); } else { - result.push_back(static_cast(UNICODE_REPLACEMENT_CHAR)); + result.push_back( + static_cast(UNICODE_REPLACEMENT_CHAR)); } } } else { // SQLWCHAR is UTF-32, so just copy with validation for (size_t i = 0; i < length; ++i) { - uint32_t cp = static_cast(sqlwStr[i]); + uint32_t cp = + static_cast(sqlwStr[i]); if (IsValidUnicodeScalar(cp)) { - result.push_back(static_cast(cp)); + result.push_back( + static_cast(cp)); } else { - result.push_back(static_cast(UNICODE_REPLACEMENT_CHAR)); + result.push_back( + static_cast(UNICODE_REPLACEMENT_CHAR)); } } } return result; } -inline std::vector WStringToSQLWCHAR(const std::wstring& str) { +inline std::vector WStringToSQLWCHAR( + const std::wstring& str) { std::vector result; result.reserve(str.size() + 2); if constexpr (sizeof(SQLWCHAR) == 2) { @@ -118,12 +138,17 @@ inline std::vector WStringToSQLWCHAR(const std::wstring& str) { } if (cp <= 0xFFFF) { // Fits in a single UTF-16 code unit - result.push_back(static_cast(cp)); + result.push_back( + static_cast(cp)); } else { // Encode as surrogate pair cp -= 0x10000; - SQLWCHAR high = static_cast((cp >> 10) + UNICODE_SURROGATE_HIGH_START); - SQLWCHAR low = static_cast((cp & 0x3FF) + UNICODE_SURROGATE_LOW_START); + SQLWCHAR high = static_cast( + (cp >> 10) + + UNICODE_SURROGATE_HIGH_START); + SQLWCHAR low = static_cast( + (cp & 0x3FF) + + UNICODE_SURROGATE_LOW_START); result.push_back(high); result.push_back(low); } @@ -133,89 +158,123 @@ inline std::vector WStringToSQLWCHAR(const std::wstring& str) { for (wchar_t wc : str) { uint32_t cp = static_cast(wc); if (IsValidUnicodeScalar(cp)) { - result.push_back(static_cast(cp)); + result.push_back( + static_cast(cp)); } else { - result.push_back(static_cast(UNICODE_REPLACEMENT_CHAR)); + result.push_back( + static_cast(UNICODE_REPLACEMENT_CHAR)); } } } - result.push_back(0); // null terminator + result.push_back(0); // null terminator return result; } #endif -#if defined(__APPLE__) || defined(__linux__) -#include "unix_utils.h" // For Unix-specific Unicode encoding fixes -#include "unix_buffers.h" // For Unix-specific buffer handling -#endif - //------------------------------------------------------------------------------------------------- // Function pointer typedefs //------------------------------------------------------------------------------------------------- // Handle APIs -typedef SQLRETURN (SQL_API* SQLAllocHandleFunc)(SQLSMALLINT, SQLHANDLE, SQLHANDLE*); -typedef SQLRETURN (SQL_API* SQLSetEnvAttrFunc)(SQLHANDLE, SQLINTEGER, SQLPOINTER, SQLINTEGER); -typedef SQLRETURN (SQL_API* SQLSetConnectAttrFunc)(SQLHDBC, SQLINTEGER, SQLPOINTER, SQLINTEGER); -typedef SQLRETURN (SQL_API* SQLSetStmtAttrFunc)(SQLHSTMT, SQLINTEGER, SQLPOINTER, SQLINTEGER); -typedef SQLRETURN (SQL_API* SQLGetConnectAttrFunc)(SQLHDBC, SQLINTEGER, SQLPOINTER, SQLINTEGER, SQLINTEGER*); +typedef SQLRETURN(SQL_API* SQLAllocHandleFunc)(SQLSMALLINT, SQLHANDLE, + SQLHANDLE*); +typedef SQLRETURN(SQL_API* SQLSetEnvAttrFunc)(SQLHANDLE, SQLINTEGER, + SQLPOINTER, SQLINTEGER); +typedef SQLRETURN(SQL_API* SQLSetConnectAttrFunc)(SQLHDBC, SQLINTEGER, + SQLPOINTER, SQLINTEGER); +typedef SQLRETURN(SQL_API* SQLSetStmtAttrFunc)(SQLHSTMT, SQLINTEGER, + SQLPOINTER, SQLINTEGER); +typedef SQLRETURN(SQL_API* SQLGetConnectAttrFunc)(SQLHDBC, SQLINTEGER, + SQLPOINTER, SQLINTEGER, + SQLINTEGER*); // Connection and Execution APIs -typedef SQLRETURN (SQL_API* SQLDriverConnectFunc)(SQLHANDLE, SQLHWND, SQLWCHAR*, SQLSMALLINT, SQLWCHAR*, - SQLSMALLINT, SQLSMALLINT*, SQLUSMALLINT); -typedef SQLRETURN (SQL_API* SQLExecDirectFunc)(SQLHANDLE, SQLWCHAR*, SQLINTEGER); -typedef SQLRETURN (SQL_API* SQLPrepareFunc)(SQLHANDLE, SQLWCHAR*, SQLINTEGER); -typedef SQLRETURN (SQL_API* SQLBindParameterFunc)(SQLHANDLE, SQLUSMALLINT, SQLSMALLINT, SQLSMALLINT, - SQLSMALLINT, SQLULEN, SQLSMALLINT, SQLPOINTER, SQLLEN, - SQLLEN*); -typedef SQLRETURN (SQL_API* SQLExecuteFunc)(SQLHANDLE); -typedef SQLRETURN (SQL_API* SQLRowCountFunc)(SQLHSTMT, SQLLEN*); -typedef SQLRETURN (SQL_API* SQLSetDescFieldFunc)(SQLHDESC, SQLSMALLINT, SQLSMALLINT, SQLPOINTER, SQLINTEGER); -typedef SQLRETURN (SQL_API* SQLGetStmtAttrFunc)(SQLHSTMT, SQLINTEGER, SQLPOINTER, SQLINTEGER, SQLINTEGER*); +typedef SQLRETURN(SQL_API* SQLDriverConnectFunc)( + SQLHANDLE, SQLHWND, SQLWCHAR*, SQLSMALLINT, SQLWCHAR*, SQLSMALLINT, + SQLSMALLINT*, SQLUSMALLINT); + +typedef SQLRETURN(SQL_API* SQLExecDirectFunc)( + SQLHANDLE, SQLWCHAR*, SQLINTEGER); + +typedef SQLRETURN(SQL_API* SQLPrepareFunc)( + SQLHANDLE, SQLWCHAR*, SQLINTEGER); + +typedef SQLRETURN(SQL_API* SQLBindParameterFunc)( + SQLHANDLE, SQLUSMALLINT, SQLSMALLINT, SQLSMALLINT, SQLSMALLINT, + SQLULEN, SQLSMALLINT, SQLPOINTER, SQLLEN, SQLLEN*); + +typedef SQLRETURN(SQL_API* SQLExecuteFunc)( + SQLHANDLE); + +typedef SQLRETURN(SQL_API* SQLRowCountFunc)( + SQLHSTMT, SQLLEN*); + +typedef SQLRETURN(SQL_API* SQLSetDescFieldFunc)( + SQLHDESC, SQLSMALLINT, SQLSMALLINT, SQLPOINTER, SQLINTEGER); + +typedef SQLRETURN(SQL_API* SQLGetStmtAttrFunc)( + SQLHSTMT, SQLINTEGER, SQLPOINTER, SQLINTEGER, SQLINTEGER*); // Data retrieval APIs -typedef SQLRETURN (SQL_API* SQLFetchFunc)(SQLHANDLE); -typedef SQLRETURN (SQL_API* SQLFetchScrollFunc)(SQLHANDLE, SQLSMALLINT, SQLLEN); -typedef SQLRETURN (SQL_API* SQLGetDataFunc)(SQLHANDLE, SQLUSMALLINT, SQLSMALLINT, SQLPOINTER, SQLLEN, - SQLLEN*); -typedef SQLRETURN (SQL_API* SQLNumResultColsFunc)(SQLHSTMT, SQLSMALLINT*); -typedef SQLRETURN (SQL_API* SQLBindColFunc)(SQLHSTMT, SQLUSMALLINT, SQLSMALLINT, SQLPOINTER, SQLLEN, - SQLLEN*); -typedef SQLRETURN (SQL_API* SQLDescribeColFunc)(SQLHSTMT, SQLUSMALLINT, SQLWCHAR*, SQLSMALLINT, - SQLSMALLINT*, SQLSMALLINT*, SQLULEN*, SQLSMALLINT*, - SQLSMALLINT*); -typedef SQLRETURN (SQL_API* SQLMoreResultsFunc)(SQLHSTMT); -typedef SQLRETURN (SQL_API* SQLColAttributeFunc)(SQLHSTMT, SQLUSMALLINT, SQLUSMALLINT, SQLPOINTER, - SQLSMALLINT, SQLSMALLINT*, SQLPOINTER); +typedef SQLRETURN(SQL_API* SQLFetchFunc)( + SQLHANDLE); + +typedef SQLRETURN(SQL_API* SQLFetchScrollFunc)( + SQLHANDLE, SQLSMALLINT, SQLLEN); + +typedef SQLRETURN(SQL_API* SQLGetDataFunc)( + SQLHANDLE, SQLUSMALLINT, SQLSMALLINT, SQLPOINTER, SQLLEN, SQLLEN*); + +typedef SQLRETURN(SQL_API* SQLNumResultColsFunc)( + SQLHSTMT, SQLSMALLINT*); + +typedef SQLRETURN(SQL_API* SQLBindColFunc)( + SQLHSTMT, SQLUSMALLINT, SQLSMALLINT, SQLPOINTER, SQLLEN, SQLLEN*); + +typedef SQLRETURN(SQL_API* SQLDescribeColFunc)( + SQLHSTMT, SQLUSMALLINT, SQLWCHAR*, SQLSMALLINT, SQLSMALLINT*, + SQLSMALLINT*, SQLULEN*, SQLSMALLINT*, SQLSMALLINT*); + +typedef SQLRETURN(SQL_API* SQLMoreResultsFunc)( + SQLHSTMT); + +typedef SQLRETURN(SQL_API* SQLColAttributeFunc)( + SQLHSTMT, SQLUSMALLINT, SQLUSMALLINT, SQLPOINTER, SQLSMALLINT, + SQLSMALLINT*, SQLPOINTER); + typedef SQLRETURN (*SQLTablesFunc)( - SQLHSTMT StatementHandle, - SQLWCHAR* CatalogName, - SQLSMALLINT NameLength1, - SQLWCHAR* SchemaName, - SQLSMALLINT NameLength2, - SQLWCHAR* TableName, - SQLSMALLINT NameLength3, - SQLWCHAR* TableType, - SQLSMALLINT NameLength4 -); - + SQLHSTMT StatementHandle, SQLWCHAR* CatalogName, SQLSMALLINT NameLength1, + SQLWCHAR* SchemaName, SQLSMALLINT NameLength2, SQLWCHAR* TableName, + SQLSMALLINT NameLength3, SQLWCHAR* TableType, SQLSMALLINT NameLength4); + // Transaction APIs -typedef SQLRETURN (SQL_API* SQLEndTranFunc)(SQLSMALLINT, SQLHANDLE, SQLSMALLINT); +typedef SQLRETURN(SQL_API* SQLEndTranFunc)( + SQLSMALLINT, SQLHANDLE, SQLSMALLINT); // Disconnect/free APIs -typedef SQLRETURN (SQL_API* SQLFreeHandleFunc)(SQLSMALLINT, SQLHANDLE); -typedef SQLRETURN (SQL_API* SQLDisconnectFunc)(SQLHDBC); -typedef SQLRETURN (SQL_API* SQLFreeStmtFunc)(SQLHSTMT, SQLUSMALLINT); +typedef SQLRETURN(SQL_API* SQLFreeHandleFunc)( + SQLSMALLINT, SQLHANDLE); + +typedef SQLRETURN(SQL_API* SQLDisconnectFunc)( + SQLHDBC); + +typedef SQLRETURN(SQL_API* SQLFreeStmtFunc)( + SQLHSTMT, SQLUSMALLINT); // Diagnostic APIs -typedef SQLRETURN (SQL_API* SQLGetDiagRecFunc)(SQLSMALLINT, SQLHANDLE, SQLSMALLINT, SQLWCHAR*, SQLINTEGER*, - SQLWCHAR*, SQLSMALLINT, SQLSMALLINT*); +typedef SQLRETURN(SQL_API* SQLGetDiagRecFunc)( + SQLSMALLINT, SQLHANDLE, SQLSMALLINT, SQLWCHAR*, SQLINTEGER*, SQLWCHAR*, + SQLSMALLINT, SQLSMALLINT*); -typedef SQLRETURN (SQL_API* SQLDescribeParamFunc)(SQLHSTMT, SQLUSMALLINT, SQLSMALLINT*, SQLULEN*, SQLSMALLINT*, SQLSMALLINT*); +typedef SQLRETURN(SQL_API* SQLDescribeParamFunc)( + SQLHSTMT, SQLUSMALLINT, SQLSMALLINT*, SQLULEN*, SQLSMALLINT*, SQLSMALLINT*); // DAE APIs -typedef SQLRETURN (SQL_API* SQLParamDataFunc)(SQLHSTMT, SQLPOINTER*); -typedef SQLRETURN (SQL_API* SQLPutDataFunc)(SQLHSTMT, SQLPOINTER, SQLLEN); +typedef SQLRETURN(SQL_API* SQLParamDataFunc)( + SQLHSTMT, SQLPOINTER*); + +typedef SQLRETURN(SQL_API* SQLPutDataFunc)( + SQLHSTMT, SQLPOINTER, SQLLEN); //------------------------------------------------------------------------------------------------- // Extern function pointer declarations (defined in ddbc_bindings.cpp) //------------------------------------------------------------------------------------------------- @@ -280,7 +339,7 @@ typedef void* DriverHandle; #endif // Platform-agnostic function to get a function pointer from the loaded library -template +template T GetFunctionPointer(DriverHandle handle, const char* functionName) { #ifdef _WIN32 // Windows: Use GetProcAddress @@ -300,23 +359,25 @@ DriverHandle LoadDriverOrThrowException(); //------------------------------------------------------------------------------------------------- // DriverLoader (Singleton) // -// Ensures the ODBC driver and all function pointers are loaded exactly once across the process. -// This avoids redundant work and ensures thread-safe, centralized initialization. +// Ensures the ODBC driver and all function pointers are loaded exactly once +// across the process. This avoids redundant work and ensures thread-safe, +// centralized initialization. // // Not copyable or assignable. //------------------------------------------------------------------------------------------------- class DriverLoader { - public: - static DriverLoader& getInstance(); - void loadDriver(); - private: - DriverLoader(); - DriverLoader(const DriverLoader&) = delete; - DriverLoader& operator=(const DriverLoader&) = delete; - - bool m_driverLoaded; - std::once_flag m_onceFlag; - }; + public: + static DriverLoader& getInstance(); + void loadDriver(); + + private: + DriverLoader(); + DriverLoader(const DriverLoader&) = delete; + DriverLoader& operator=(const DriverLoader&) = delete; + + bool m_driverLoaded; + std::once_flag m_onceFlag; +}; //------------------------------------------------------------------------------------------------- // SqlHandle @@ -325,36 +386,43 @@ class DriverLoader { // Use `std::shared_ptr` (alias: SqlHandlePtr) for shared ownership. //------------------------------------------------------------------------------------------------- class SqlHandle { - public: - SqlHandle(SQLSMALLINT type, SQLHANDLE rawHandle); - ~SqlHandle(); - SQLHANDLE get() const; - SQLSMALLINT type() const; - void free(); - private: - SQLSMALLINT _type; - SQLHANDLE _handle; - }; - using SqlHandlePtr = std::shared_ptr; - -// This struct is used to relay error info obtained from SQLDiagRec API to the Python module + public: + SqlHandle(SQLSMALLINT type, SQLHANDLE rawHandle); + ~SqlHandle(); + SQLHANDLE get() const; + SQLSMALLINT type() const; + void free(); + + private: + SQLSMALLINT _type; + SQLHANDLE _handle; +}; +using SqlHandlePtr = std::shared_ptr; + +// This struct is used to relay error info obtained from SQLDiagRec API to the +// Python module struct ErrorInfo { std::wstring sqlState; std::wstring ddbcErrorMsg; }; -ErrorInfo SQLCheckError_Wrap(SQLSMALLINT handleType, SqlHandlePtr handle, SQLRETURN retcode); +ErrorInfo SQLCheckError_Wrap(SQLSMALLINT handleType, SqlHandlePtr handle, + SQLRETURN retcode); inline std::string WideToUTF8(const std::wstring& wstr) { if (wstr.empty()) return {}; #if defined(_WIN32) - int size_needed = WideCharToMultiByte(CP_UTF8, 0, wstr.data(), static_cast(wstr.size()), nullptr, 0, nullptr, nullptr); + int size_needed = WideCharToMultiByte( + CP_UTF8, 0, wstr.data(), static_cast(wstr.size()), + nullptr, 0, nullptr, nullptr); if (size_needed == 0) return {}; std::string result(size_needed, 0); - int converted = WideCharToMultiByte(CP_UTF8, 0, wstr.data(), static_cast(wstr.size()), result.data(), size_needed, nullptr, nullptr); + int converted = WideCharToMultiByte( + CP_UTF8, 0, wstr.data(), static_cast(wstr.size()), + result.data(), size_needed, nullptr, nullptr); if (converted == 0) return {}; return result; #else - std::wstring_convert> converter; + std::wstring_convert > converter; return converter.to_bytes(wstr); #endif } @@ -362,17 +430,20 @@ inline std::string WideToUTF8(const std::wstring& wstr) { inline std::wstring Utf8ToWString(const std::string& str) { if (str.empty()) return {}; #if defined(_WIN32) - int size_needed = MultiByteToWideChar(CP_UTF8, 0, str.data(), static_cast(str.size()), nullptr, 0); + int size_needed = MultiByteToWideChar( + CP_UTF8, 0, str.data(), static_cast(str.size()), nullptr, 0); if (size_needed == 0) { LOG("MultiByteToWideChar failed."); return {}; } std::wstring result(size_needed, 0); - int converted = MultiByteToWideChar(CP_UTF8, 0, str.data(), static_cast(str.size()), result.data(), size_needed); + int converted = MultiByteToWideChar( + CP_UTF8, 0, str.data(), static_cast(str.size()), + result.data(), size_needed); if (converted == 0) return {}; return result; #else - std::wstring_convert> converter; + std::wstring_convert > converter; return converter.from_bytes(str); #endif } diff --git a/mssql_python/pybind/unix_buffers.h b/mssql_python/pybind/unix_buffers.h index 57039ac8..38728da2 100644 --- a/mssql_python/pybind/unix_buffers.h +++ b/mssql_python/pybind/unix_buffers.h @@ -1,7 +1,7 @@ /** * Copyright (c) Microsoft Corporation. * Licensed under the MIT license. - * + * * This file provides utilities for handling character encoding and buffer management * specifically for macOS ODBC operations. It implements functionality similar to * the UCS_dec function in the Python PoC. @@ -9,12 +9,13 @@ #pragma once -#include -#include -#include #include #include +#include +#include +#include + namespace unix_buffers { // Constants for Unicode character encoding @@ -26,11 +27,11 @@ constexpr size_t UCS_LENGTH = 2; * handling memory allocation and conversion to std::wstring. */ class SQLWCHARBuffer { -private: + private: std::unique_ptr buffer; size_t buffer_size; -public: + public: /** * Constructor allocates a buffer of the specified size */ @@ -45,16 +46,12 @@ class SQLWCHARBuffer { /** * Returns the data pointer for use with ODBC functions */ - SQLWCHAR* data() { - return buffer.get(); - } + SQLWCHAR* data() { return buffer.get(); } /** * Returns the size of the buffer */ - size_t size() const { - return buffer_size; - } + size_t size() const { return buffer_size; } /** * Converts the SQLWCHAR buffer to std::wstring @@ -62,7 +59,7 @@ class SQLWCHARBuffer { */ std::wstring toString(SQLSMALLINT length = -1) const { std::wstring result; - + // If length is provided, use it if (length > 0) { for (SQLSMALLINT i = 0; i < length; i++) { @@ -70,7 +67,7 @@ class SQLWCHARBuffer { } return result; } - + // Otherwise, read until null terminator for (size_t i = 0; i < buffer_size; i++) { if (buffer[i] == 0) { @@ -78,7 +75,7 @@ class SQLWCHARBuffer { } result.push_back(static_cast(buffer[i])); } - + return result; } }; @@ -88,56 +85,53 @@ class SQLWCHARBuffer { * Similar to the error list handling in the Python PoC _check_ret function */ class DiagnosticRecords { -private: + private: struct Record { std::wstring sqlState; std::wstring message; SQLINTEGER nativeError; }; - + std::vector records; -public: - void addRecord(const std::wstring& sqlState, const std::wstring& message, SQLINTEGER nativeError) { + public: + void addRecord(const std::wstring& sqlState, const std::wstring& message, + SQLINTEGER nativeError) { records.push_back({sqlState, message, nativeError}); } - - bool empty() const { - return records.empty(); - } - + + bool empty() const { return records.empty(); } + std::wstring getSQLState() const { if (!records.empty()) { return records[0].sqlState; } - return L"HY000"; // General error + return L"HY000"; // General error } - + std::wstring getFirstErrorMessage() const { if (!records.empty()) { return records[0].message; } return L"Unknown error"; } - + std::wstring getFullErrorMessage() const { if (records.empty()) { return L"No error information available"; } - + std::wstring fullMessage = records[0].message; - + // Add additional error messages if there are any for (size_t i = 1; i < records.size(); i++) { fullMessage += L"; [" + records[i].sqlState + L"] " + records[i].message; } - + return fullMessage; } - - size_t size() const { - return records.size(); - } + + size_t size() const { return records.size(); } }; /** @@ -147,23 +141,23 @@ class DiagnosticRecords { inline std::wstring UCS_dec(const SQLWCHAR* buffer, size_t maxLength = 0) { std::wstring result; size_t i = 0; - + while (true) { // Break if we've reached the maximum length if (maxLength > 0 && i >= maxLength) { break; } - + // Break if we've reached a null terminator if (buffer[i] == 0) { break; } - + result.push_back(static_cast(buffer[i])); i++; } - + return result; } -} // namespace unix_buffers +} // namespace unix_buffers diff --git a/mssql_python/pybind/unix_utils.cpp b/mssql_python/pybind/unix_utils.cpp index c98a9e09..60665d8f 100644 --- a/mssql_python/pybind/unix_utils.cpp +++ b/mssql_python/pybind/unix_utils.cpp @@ -35,27 +35,29 @@ void LOG(const std::string& formatString, Args&&... args) { // Function to convert SQLWCHAR strings to std::wstring on macOS std::wstring SQLWCHARToWString(const SQLWCHAR* sqlwStr, size_t length = SQL_NTS) { if (!sqlwStr) return std::wstring(); - + if (length == SQL_NTS) { // Determine length if not provided size_t i = 0; while (sqlwStr[i] != 0) ++i; length = i; } - + // Create a UTF-16LE byte array from the SQLWCHAR array std::vector utf16Bytes(length * kUcsLength); for (size_t i = 0; i < length; ++i) { // Copy each SQLWCHAR (2 bytes) to the byte array memcpy(&utf16Bytes[i * kUcsLength], &sqlwStr[i], kUcsLength); } - + // Convert UTF-16LE to std::wstring (UTF-32 on macOS) try { // Use C++11 codecvt to convert between UTF-16LE and wstring - std::wstring_convert> converter; - return converter.from_bytes(reinterpret_cast(utf16Bytes.data()), - reinterpret_cast(utf16Bytes.data() + utf16Bytes.size())); + std::wstring_convert> + converter; + return converter.from_bytes( + reinterpret_cast(utf16Bytes.data()), + reinterpret_cast(utf16Bytes.data() + utf16Bytes.size())); } catch (const std::exception& e) { // Log a warning about using fallback conversion LOG("Warning: Using fallback string conversion on macOS. Character data might be inexact."); @@ -73,18 +75,21 @@ std::wstring SQLWCHARToWString(const SQLWCHAR* sqlwStr, size_t length = SQL_NTS) std::vector WStringToSQLWCHAR(const std::wstring& str) { try { // Convert wstring (UTF-32 on macOS) to UTF-16LE bytes - std::wstring_convert> converter; + std::wstring_convert> + converter; std::string utf16Bytes = converter.to_bytes(str); - + // Convert the bytes to SQLWCHAR array - std::vector result(utf16Bytes.size() / kUcsLength + 1, 0); // +1 for null terminator + std::vector result(utf16Bytes.size() / kUcsLength + 1, + 0); // +1 for null terminator for (size_t i = 0; i < utf16Bytes.size() / kUcsLength; ++i) { memcpy(&result[i], &utf16Bytes[i * kUcsLength], kUcsLength); } return result; } catch (const std::exception& e) { // Log a warning about using fallback conversion - LOG("Warning: Using fallback conversion for std::wstring to SQLWCHAR on macOS. Character data might be inexact."); + LOG("Warning: Using fallback conversion for std::wstring to SQLWCHAR on macOS. Character " + "data might be inexact."); // Fallback to simple casting if codecvt fails std::vector result(str.size() + 1, 0); // +1 for null terminator for (size_t i = 0; i < str.size(); ++i) { @@ -98,7 +103,7 @@ std::vector WStringToSQLWCHAR(const std::wstring& str) { // based on your ctypes UCS_dec implementation std::string SQLWCHARToUTF8String(const SQLWCHAR* buffer) { if (!buffer) return ""; - + std::vector utf16Bytes; size_t i = 0; while (buffer[i] != 0) { @@ -108,14 +113,17 @@ std::string SQLWCHARToUTF8String(const SQLWCHAR* buffer) { utf16Bytes.push_back(bytes[1]); i++; } - + try { - std::wstring_convert> converter; - return converter.to_bytes(reinterpret_cast(utf16Bytes.data()), - reinterpret_cast(utf16Bytes.data() + utf16Bytes.size())); + std::wstring_convert> + converter; + return converter.to_bytes( + reinterpret_cast(utf16Bytes.data()), + reinterpret_cast(utf16Bytes.data() + utf16Bytes.size())); } catch (const std::exception& e) { // Log a warning about using fallback conversion - LOG("Warning: Using fallback conversion for SQLWCHAR to UTF-8 on macOS. Character data might be inexact."); + LOG("Warning: Using fallback conversion for SQLWCHAR to UTF-8 on macOS. Character data " + "might be inexact."); // Simple fallback conversion std::string result; for (size_t j = 0; j < i; ++j) { diff --git a/mssql_python/pybind/unix_utils.h b/mssql_python/pybind/unix_utils.h index cad35e74..2d49c5d2 100644 --- a/mssql_python/pybind/unix_utils.h +++ b/mssql_python/pybind/unix_utils.h @@ -8,13 +8,14 @@ #pragma once -#include -#include -#include -#include +#include #include #include -#include + +#include +#include +#include +#include namespace py = pybind11; From 685728582b419bff4005053e5333df88b25c651d Mon Sep 17 00:00:00 2001 From: Jahnvi Thakkar Date: Tue, 9 Sep 2025 11:07:52 +0530 Subject: [PATCH 04/20] Resolving conflicts --- mssql_python/msvcp140.dll | Bin 0 -> 575592 bytes mssql_python/pybind/ddbc_bindings.cpp | 6 ++++-- 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 mssql_python/msvcp140.dll diff --git a/mssql_python/msvcp140.dll b/mssql_python/msvcp140.dll new file mode 100644 index 0000000000000000000000000000000000000000..0a9b13d7585510f6f52e6c3649e26236d1e77c55 GIT binary patch literal 575592 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

jwKIraloi_ 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⁢OlVtmx4cX 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 duVT!w=AD>`VqBYw5Xqe8!9rMTTIgmg_ #include #include // std::setw, std::setfill #include @@ -1710,6 +1711,7 @@ SQLRETURN BindParameterArray(SQLHANDLE hStmt, } std::memcpy(wcharArray + i * (info.columnSize + 1), wstr.c_str(), (wstr.length() + 1) * sizeof(SQLWCHAR)); strLenOrIndArray[i] = SQL_NTS; +#endif } } dataPtr = wcharArray; @@ -2330,8 +2332,8 @@ SQLRETURN SQLGetData_wrap( row.append(std::wstring(dataBuffer.data())); #endif } else { - // In this case, buffer size is - // smaller, and data to be retrieved is longer + // In this case, buffer size is smaller, + // and data to be retrieved is longer // TODO: Revisit std::ostringstream oss; oss << "Buffer length for fetch (" << From 79434439b4a80095eb90020ecfe7451e49c92131 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Tue, 9 Sep 2025 11:29:43 +0530 Subject: [PATCH 05/20] silence unwanted devskim alerts --- .github/workflows/devskim.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/devskim.yml b/.github/workflows/devskim.yml index 86f469d4..9a71f870 100644 --- a/.github/workflows/devskim.yml +++ b/.github/workflows/devskim.yml @@ -32,3 +32,6 @@ jobs: uses: github/codeql-action/upload-sarif@v3 with: sarif_file: devskim-results.sarif + # Exclude TODO (DS176209) and localhost (DS162092) alerts + # Since they are used only for future improvements & tests respectively + exclude-rules: DS176209,DS162092 From 6892100674cade8adb727ee056ec2c382d18bddc Mon Sep 17 00:00:00 2001 From: Jahnvi Thakkar Date: Tue, 9 Sep 2025 11:57:59 +0530 Subject: [PATCH 06/20] Resolving error --- mssql_python/pybind/ddbc_bindings.cpp | 247 +++++++++++++++++--------- mssql_python/pybind/ddbc_bindings.h | 4 +- mssql_python/pybind/unix_buffers.h | 12 +- mssql_python/pybind/unix_utils.cpp | 5 + 4 files changed, 178 insertions(+), 90 deletions(-) diff --git a/mssql_python/pybind/ddbc_bindings.cpp b/mssql_python/pybind/ddbc_bindings.cpp index 0eee729b..f464774b 100644 --- a/mssql_python/pybind/ddbc_bindings.cpp +++ b/mssql_python/pybind/ddbc_bindings.cpp @@ -51,7 +51,7 @@ struct ParamInfo { SQLSMALLINT paramSQLType; SQLULEN columnSize; SQLSMALLINT decimalDigits; - SQLLEN strLenOrInd = 0; // Required for DAE + SQLLEN strLenOrInd = 0; // Required for DAE bool isDAE = false; // Indicates if we need to stream py::object dataPtr; }; @@ -63,7 +63,7 @@ struct NumericData { SQLCHAR precision; SQLSCHAR scale; SQLCHAR sign; // 1=pos, 0=neg - std::uint64_t val; // 123.45 -> 12345 + std::uint64_t val; // 123.45 -> 12345 NumericData() : precision(0), scale(0), sign(0), val(0) {} @@ -323,7 +323,8 @@ SQLRETURN BindParameters(SQLHANDLE hStmt, const py::list& params, } if (paramInfo.isDAE) { // deferred execution - LOG("Parameter[{}] is marked for DAE streaming", paramIndex); + LOG("Parameter[{}] is marked for" + " DAE streaming", paramIndex); dataPtr = const_cast( reinterpret_cast( @@ -346,7 +347,6 @@ SQLRETURN BindParameters(SQLHANDLE hStmt, const py::list& params, bufferLength = sqlwcharBuffer->size() * sizeof(SQLWCHAR); strLenOrIndPtr = AllocateParamBuffer(paramBuffers); *strLenOrIndPtr = SQL_NTS; - } break; } @@ -526,7 +526,8 @@ SQLRETURN BindParameters(SQLHANDLE hStmt, const py::list& params, break; } case SQL_C_TYPE_DATE: { - py::object dateType = py::module_::import("datetime").attr("date"); + py::object dateType = + py::module_::import("datetime").attr("date"); if (!py::isinstance(param, dateType)) { ThrowStdException( MakeParamMismatchErrorStr( @@ -542,7 +543,7 @@ SQLRETURN BindParameters(SQLHANDLE hStmt, const py::list& params, } // TODO: can be moved to python by registering // SQL_DATE_STRUCT in pybind - SQL_DATE_STRUCT* sqlDatePtr = + SQL_DATE_STRUCT* sqlDatePtr = AllocateParamBuffer(paramBuffers); sqlDatePtr->year = static_cast(param.attr("year").cast()); @@ -564,7 +565,7 @@ SQLRETURN BindParameters(SQLHANDLE hStmt, const py::list& params, } // TODO: can be moved to python by registering // SQL_TIME_STRUCT in pybind - SQL_TIME_STRUCT* sqlTimePtr = + SQL_TIME_STRUCT* sqlTimePtr = AllocateParamBuffer(paramBuffers); sqlTimePtr->hour = static_cast(param.attr("hour").cast()); @@ -649,7 +650,10 @@ SQLRETURN BindParameters(SQLHANDLE hStmt, const py::list& params, ThrowStdException(errorString.str()); } } - assert(SQLBindParameter_ptr && SQLGetStmtAttr_ptr && SQLSetDescField_ptr); + assert( + SQLBindParameter_ptr && + SQLGetStmtAttr_ptr && + SQLSetDescField_ptr); RETCODE rc = SQLBindParameter_ptr( hStmt, @@ -685,7 +689,8 @@ SQLRETURN BindParameters(SQLHANDLE hStmt, const py::list& params, (SQLPOINTER) SQL_C_NUMERIC, 0); if (!SQL_SUCCEEDED(rc)) { LOG( - "Error when setting descriptor field SQL_DESC_TYPE - {}", + "Error when setting descriptor" + " field SQL_DESC_TYPE - {}", paramIndex); return rc; } @@ -725,7 +730,7 @@ SQLRETURN BindParameters(SQLHANDLE hStmt, const py::list& params, return SQL_SUCCESS; } -// This is temporary hack to avoid crash when +// This is temporary hack to avoid crash when // SQLDescribeCol returns 0 as columnSize // for NVARCHAR(MAX) & similar types. // Variable length data needs more nuanced handling. @@ -1311,7 +1316,6 @@ SQLRETURN SQLTables_wrap(SqlHandlePtr StatementHandle, const std::wstring& schema, const std::wstring& table, const std::wstring& tableType) { - if (!SQLTables_ptr) { LOG("Function pointer not initialized. Loading the driver."); DriverLoader::getInstance().loadDriver(); @@ -1493,7 +1497,9 @@ SQLRETURN SQLExecute_wrap(const SqlHandlePtr statementHandle, if (rc == SQL_NEED_DATA) { LOG("Beginning SQLParamData/SQLPutData loop for DAE."); SQLPOINTER paramToken = nullptr; - while ((rc = SQLParamData_ptr(hStmt, ¶mToken)) == SQL_NEED_DATA) { + while ( + (rc = SQLParamData_ptr(hStmt, ¶mToken)) == + SQL_NEED_DATA) { // Finding the paramInfo that matches the returned token const ParamInfo* matchedInfo = nullptr; for (auto& info : paramInfos) { @@ -1538,7 +1544,8 @@ SQLRETURN SQLExecute_wrap(const SqlHandlePtr statementHandle, static_cast( std::numeric_limits::max())) { ThrowStdException( - "Chunk size exceeds maximum allowed by SQLLEN"); + "Chunk size exceeds " + "maximum allowed by SQLLEN"); } rc = SQLPutData_ptr( hStmt, @@ -1618,7 +1625,8 @@ SQLRETURN BindParameterArray(SQLHANDLE hStmt, int paramIndex = 0; paramIndex < columnwise_params.size(); ++paramIndex) { - const py::list& columnValues = columnwise_params[paramIndex].cast(); + const py::list& columnValues = + columnwise_params[paramIndex].cast(); const ParamInfo& info = paramInfos[paramIndex]; if (columnValues.size() != paramSetSize) { ThrowStdException( @@ -1688,20 +1696,36 @@ SQLRETURN BindParameterArray(SQLHANDLE hStmt, 0, (info.columnSize + 1) * sizeof(SQLWCHAR)); } else { - std::wstring wstr = columnValues[i].cast(); + std::wstring wstr = + columnValues[i].cast(); #if defined(__APPLE__) || defined(__linux__) - // Convert to UTF-16 first, then check the actual UTF-16 length + // Convert to UTF-16 first, + // then check the actual UTF-16 length auto utf16Buf = WStringToSQLWCHAR(wstr); - // Check UTF-16 length (excluding null terminator) against column size - if (utf16Buf.size() > 0 && (utf16Buf.size() - 1) > info.columnSize) { + // Check UTF-16 length (excluding null terminator) + // against column size + if ( + utf16Buf.size() > 0 && + (utf16Buf.size() - 1) > info.columnSize) { std::string offending = WideToUTF8(wstr); - ThrowStdException("Input string UTF-16 length exceeds allowed column size at parameter index " + std::to_string(paramIndex) + - ". UTF-16 length: " + std::to_string(utf16Buf.size() - 1) + ", Column size: " + std::to_string(info.columnSize)); + ThrowStdException( + "Input string UTF-16 length exceeds" + " allowed column size at parameter index " + + std::to_string(paramIndex) + + ". UTF-16 length: " + + std::to_string(utf16Buf.size() - 1) + + ", Column size: " + + std::to_string(info.columnSize)); } - // If we reach here, the UTF-16 string fits - copy it completely - std::memcpy(wcharArray + i * (info.columnSize + 1), utf16Buf.data(), utf16Buf.size() * sizeof(SQLWCHAR)); + // If we reach here, the UTF-16 string fits + // - copy it completely + std::memcpy( + wcharArray + i * (info.columnSize + 1), + utf16Buf.data(), + utf16Buf.size() * sizeof(SQLWCHAR)); #else - // On Windows, wchar_t is already UTF-16, so the original check is sufficient + // On Windows, wchar_t is already UTF-16, + // so the original check is sufficient if (wstr.length() > info.columnSize) { std::string offending = WideToUTF8(wstr); ThrowStdException( @@ -1709,7 +1733,10 @@ SQLRETURN BindParameterArray(SQLHANDLE hStmt, " size at parameter index " + std::to_string(paramIndex)); } - std::memcpy(wcharArray + i * (info.columnSize + 1), wstr.c_str(), (wstr.length() + 1) * sizeof(SQLWCHAR)); + std::memcpy( + wcharArray + i * (info.columnSize + 1), + wstr.c_str(), + (wstr.length() + 1) * sizeof(SQLWCHAR)); strLenOrIndArray[i] = SQL_NTS; #endif } @@ -1799,7 +1826,8 @@ SQLRETURN BindParameterArray(SQLHANDLE hStmt, std::memcpy( charArray + i * (info.columnSize + 1), str.c_str(), str.size()); - strLenOrIndArray[i] = static_cast(str.size()); + strLenOrIndArray[i] = + static_cast(str.size()); } } dataPtr = charArray; @@ -1900,7 +1928,8 @@ SQLRETURN BindParameterArray(SQLHANDLE hStmt, for (size_t i = 0; i < paramSetSize; ++i) { if (columnValues[i].is_none()) { strLenOrIndArray[i] = SQL_NULL_DATA; - std::memset(&dateArray[i], 0, sizeof(SQL_DATE_STRUCT)); + std::memset(&dateArray[i], 0, + sizeof(SQL_DATE_STRUCT)); } else { py::object dateObj = columnValues[i]; dateArray[i].year = @@ -1947,7 +1976,7 @@ SQLRETURN BindParameterArray(SQLHANDLE hStmt, SQL_TIMESTAMP_STRUCT* tsArray = AllocateParamBufferArray( tempBuffers, paramSetSize); - strLenOrIndArray = + strLenOrIndArray = AllocateParamBufferArray( tempBuffers, paramSetSize); for (size_t i = 0; i < paramSetSize; ++i) { @@ -2012,9 +2041,10 @@ SQLRETURN BindParameterArray(SQLHANDLE hStmt, std::memset(numericArray[i].val, 0, sizeof(numericArray[i].val)); std::memcpy(numericArray[i].val, - reinterpret_cast(&decimalParam.val), - std::min(sizeof(decimalParam.val), - sizeof(numericArray[i].val))); + reinterpret_cast( + &decimalParam.val), + std::min(sizeof(decimalParam.val), + sizeof(numericArray[i].val))); strLenOrIndArray[i] = sizeof(SQL_NUMERIC_STRUCT); } dataPtr = numericArray; @@ -2103,7 +2133,7 @@ SQLSMALLINT SQLNumResultCols_wrap(SqlHandlePtr statementHandle) { // Wrap SQLDescribeCol SQLRETURN SQLDescribeCol_wrap( - SqlHandlePtr StatementHandle, + SqlHandlePtr StatementHandle, py::list& ColumnMetadata) { LOG("Get column description"); if (!SQLDescribeCol_ptr) { @@ -2220,7 +2250,7 @@ SQLRETURN SQLGetData_wrap( &dataLen); if (SQL_SUCCEEDED(ret)) { - // TODO: Refactor these if's across + // TODO: Refactor these if's across // other switches to avoid code duplication // columnSize is in chars, dataLen is in bytes if (dataLen > 0) { @@ -2260,21 +2290,28 @@ SQLRETURN SQLGetData_wrap( << dataType; ThrowStdException(oss.str()); } - } else if (dataLen == SQL_NULL_DATA) { - row.append(py::none()); + } else if (dataLen == SQL_NULL_DATA) { + row.append(py::none()); } else if (dataLen == 0) { // Handle zero-length (non-NULL) data row.append(std::string("")); } else if (dataLen == SQL_NO_TOTAL) { - // This means the length of the data couldn't be determined - LOG("SQLGetData couldn't determine the length of the data. " - "Returning NULL value instead. Column ID - {}, Data Type - {}", i, dataType); + // This means the length of the + // data couldn't be determined + LOG("SQLGetData couldn't determine " + "the length of the data. " + "Returning NULL value instead." + " Column ID - {}, Data Type - {}", i, dataType); } else if (dataLen < 0) { // This is unexpected - LOG("SQLGetData returned an unexpected negative data length. " - "Raising exception. Column ID - {}, Data Type - {}, Data Length - {}", + LOG("SQLGetData returned an unexpected" + " negative data length. " + "Raising exception. Column ID - {}," + " Data Type - {}, Data Length - {}", i, dataType, dataLen); - ThrowStdException("SQLGetData returned an unexpected negative data length"); + ThrowStdException( + "SQLGetData returned an " + "unexpected negative data length"); } } else { LOG("Error retrieving data for column - {}," @@ -2304,7 +2341,7 @@ SQLRETURN SQLGetData_wrap( // TODO: Refactor these if's across other // switches to avoid code duplication if (dataLen > 0) { - uint64_t numCharsInData = + uint64_t numCharsInData = dataLen / sizeof(SQLWCHAR); if (numCharsInData < dataBuffer.size()) { // SQLGetData will null-terminate the data @@ -2315,7 +2352,8 @@ SQLRETURN SQLGetData_wrap( size_t actualBufferSize = dataBuffer.size() * sizeof(SQLWCHAR); if (dataLen < 0 || - static_cast(dataLen) > actualBufferSize) { + static_cast(dataLen) > + actualBufferSize) { LOG( "Error: py::bytes creation request" " exceeds buffer size. dataLen={} " @@ -2343,17 +2381,21 @@ SQLRETURN SQLGetData_wrap( << i << ", datatype - " << dataType; ThrowStdException(oss.str()); } - } else if (dataLen == SQL_NULL_DATA) { - row.append(py::none()); + } else if (dataLen == SQL_NULL_DATA) { + row.append(py::none()); } else if (dataLen == 0) { // Handle zero-length (non-NULL) data row.append(py::str("")); } else if (dataLen < 0) { // This is unexpected - LOG("SQLGetData returned an unexpected negative data length. " - "Raising exception. Column ID - {}, Data Type - {}, Data Length - {}", + LOG("SQLGetData returned an" + " unexpected negative data length. " + "Raising exception. Column ID - {}," + " Data Type - {}, Data Length - {}", i, dataType, dataLen); - ThrowStdException("SQLGetData returned an unexpected negative data length"); + ThrowStdException( + "SQLGetData returned an unexpected" + " negative data length"); } } else { LOG("Error retrieving data for column - {}," @@ -2419,7 +2461,8 @@ SQLRETURN SQLGetData_wrap( if (SQL_SUCCEEDED(ret)) { try { - // Convert numericStr to py::decimal.Decimal and append to row + // Convert numericStr to py::decimal. + // Decimal and append to row row.append( py::module_::import("decimal").attr("Decimal")( std::string( @@ -2564,7 +2607,8 @@ SQLRETURN SQLGetData_wrap( // switches to avoid code duplication if (dataLen > 0) { if (static_cast(dataLen) <= columnSize) { - row.append(py::bytes(reinterpret_cast( + row.append( + py::bytes(reinterpret_cast( dataBuffer.get()), dataLen)); } else { // In this case, buffer size is smaller, @@ -2579,17 +2623,21 @@ SQLRETURN SQLGetData_wrap( dataType; ThrowStdException(oss.str()); } - } else if (dataLen == SQL_NULL_DATA) { - row.append(py::none()); + } else if (dataLen == SQL_NULL_DATA) { + row.append(py::none()); } else if (dataLen == 0) { // Empty bytes row.append(py::bytes("")); } else if (dataLen < 0) { // This is unexpected - LOG("SQLGetData returned an unexpected negative data length. " - "Raising exception. Column ID - {}, Data Type - {}, Data Length - {}", + LOG("SQLGetData returned an unexpected" + " negative data length. " + "Raising exception. Column ID - {}," + " Data Type - {}, Data Length - {}", i, dataType, dataLen); - ThrowStdException("SQLGetData returned an unexpected negative data length"); + ThrowStdException( + "SQLGetData returned an" + " unexpected negative data length"); } } else { LOG("Error retrieving data for column - {}," @@ -2746,18 +2794,23 @@ SQLRETURN SQLBindColums(SQLHSTMT hStmt, ColumnBuffers& buffers, HandleZeroColumnSizeAtFetch(columnSize); // null-terminator uint64_t fetchBufferSize = columnSize + 1; - // TODO: For LONGVARCHAR/BINARY types, columnSize is returned as 2GB-1 by - // SQLDescribeCol. So fetchBufferSize = 2GB. fetchSize=1 if columnSize>1GB. + // TODO: For LONGVARCHAR/BINARY types, + // columnSize is returned as 2GB-1 by + // SQLDescribeCol. So fetchBufferSize = 2GB. + // fetchSize=1 if columnSize>1GB. // So we'll allocate a vector of size 2GB. // If a query fetches multiple (say N) - // LONG... columns, we will have allocated multiple (N) 2GB sized vectors. + // LONG... columns, we will have + // allocated multiple (N) 2GB sized vectors. // This will make driver very slow. And if the N is high enough, // we could hit the OS - // limit for heap memory that we can allocate, & hence get a std::bad_alloc. + // limit for heap memory that we can allocate, + // & hence get a std::bad_alloc. // The process could also be killed by OS for consuming too much memory. // Hence this will be revisited in beta to not allocate 2GB+ memory, // & use streaming instead - buffers.charBuffers[col - 1].resize(fetchSize * fetchBufferSize); + buffers.charBuffers[col - 1].resize( + fetchSize * fetchBufferSize); ret = SQLBindCol_ptr( hStmt, col, SQL_C_CHAR, buffers.charBuffers[col - 1].data(), @@ -2773,7 +2826,8 @@ SQLRETURN SQLBindColums(SQLHSTMT hStmt, ColumnBuffers& buffers, HandleZeroColumnSizeAtFetch(columnSize); // null-terminator uint64_t fetchBufferSize = columnSize + 1; - buffers.wcharBuffers[col - 1].resize(fetchSize * fetchBufferSize); + buffers.wcharBuffers[col - 1].resize( + fetchSize * fetchBufferSize); ret = SQLBindCol_ptr( hStmt, col, SQL_C_WCHAR, buffers.wcharBuffers[col - 1].data(), @@ -2894,7 +2948,8 @@ SQLRETURN SQLBindColums(SQLHSTMT hStmt, ColumnBuffers& buffers, // TODO: handle variable length data correctly. // This logic wont suffice HandleZeroColumnSizeAtFetch(columnSize); - buffers.charBuffers[col - 1].resize(fetchSize * columnSize); + buffers.charBuffers[col - 1].resize( + fetchSize * columnSize); ret = SQLBindCol_ptr( hStmt, col, SQL_C_BINARY, buffers.charBuffers[col - 1].data(), @@ -2930,7 +2985,8 @@ SQLRETURN SQLBindColums(SQLHSTMT hStmt, ColumnBuffers& buffers, } // Fetch rows in batches -// TODO: Move to anonymous namespace, since it is not used outside this file +// TODO: Move to anonymous namespace, +// since it is not used outside this file SQLRETURN FetchBatchData( SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& columnNames, @@ -2973,27 +3029,51 @@ SQLRETURN FetchBatchData( row.append(py::none()); continue; } else if (dataLen == SQL_NULL_DATA) { - LOG("Column data is NULL. Appending None to the result row. Column ID - {}", col); + LOG( + "Column data is NULL." + " Appending None to the result row." + " Column ID - {}", + col); row.append(py::none()); continue; } else if (dataLen == 0) { // Handle zero-length (non-NULL) data - if (dataType == SQL_CHAR || dataType == SQL_VARCHAR || dataType == SQL_LONGVARCHAR) { + if ( + dataType == SQL_CHAR || + dataType == SQL_VARCHAR || + dataType == SQL_LONGVARCHAR) { row.append(std::string("")); - } else if (dataType == SQL_WCHAR || dataType == SQL_WVARCHAR || dataType == SQL_WLONGVARCHAR) { + } else if ( + dataType == SQL_WCHAR || + dataType == SQL_WVARCHAR || + dataType == SQL_WLONGVARCHAR) { row.append(std::wstring(L"")); - } else if (dataType == SQL_BINARY || dataType == SQL_VARBINARY || dataType == SQL_LONGVARBINARY) { + } else if ( + dataType == SQL_BINARY || + dataType == SQL_VARBINARY || + dataType == SQL_LONGVARBINARY) { row.append(py::bytes("")); } else { - // For other datatypes, 0 length is unexpected. Log & append None - LOG("Column data length is 0 for non-string/binary datatype. Appending None to the result row. Column ID - {}", col); + // For other datatypes, 0 length is unexpected. + // Log & append None + LOG( + "Column data length is 0 for " + "non-string/binary datatype." + " Appending None to the result row." + " Column ID - {}", + col); row.append(py::none()); } continue; } else if (dataLen < 0) { - // Negative value is unexpected, log column index, SQL type & raise exception - LOG("Unexpected negative data length. Column ID - {}, SQL Type - {}, Data Length - {}", col, dataType, dataLen); - ThrowStdException("Unexpected negative data length, check logs for details"); + // Negative value is unexpected, + // log column index, SQL type & raise exception + LOG( + "Unexpected negative data length." + " Column ID - {}, SQL Type - {}, Data Length - {}", + col, dataType, dataLen); + ThrowStdException( + "Unexpected negative data length, check logs for details"); } assert(dataLen > 0 && "Data length must be > 0"); @@ -3059,10 +3139,11 @@ SQLRETURN FetchBatchData( #else // On Windows, wchar_t and SQLWCHAR are both 2 bytes, // so direct cast works - row.append(std::wstring( - reinterpret_cast( - &buffers.wcharBuffers[col - 1][i * fetchBufferSize]), - numCharsInData)); + row.append( + std::wstring( + reinterpret_cast( + &buffers.wcharBuffers[col - 1][i * fetchBufferSize]), + numCharsInData)); #endif } else { // In this case, buffer size is smaller, @@ -3107,14 +3188,14 @@ SQLRETURN FetchBatchData( case SQL_DECIMAL: case SQL_NUMERIC: { try { - // Convert numericStr to py::decimal. - // Decimal and append to row - row.append( - py::module_::import("decimal").attr("Decimal")( - std::string( - reinterpret_cast( - &buffers.charBuffers[col - 1][i * MAX_DIGITS_IN_NUMERIC]), - buffers.indicators[col - 1][i]))); + // Convert numericStr to py::decimal. + // Decimal and append to row + row.append( + py::module_::import("decimal").attr("Decimal")( + std::string( + reinterpret_cast( + &buffers.charBuffers[col - 1][i * MAX_DIGITS_IN_NUMERIC]), + buffers.indicators[col - 1][i]))); } catch (const py::error_already_set& e) { // Handle the exception, e.g., // log the error and append py::none() diff --git a/mssql_python/pybind/ddbc_bindings.h b/mssql_python/pybind/ddbc_bindings.h index f46bf42d..eae70d80 100644 --- a/mssql_python/pybind/ddbc_bindings.h +++ b/mssql_python/pybind/ddbc_bindings.h @@ -42,8 +42,8 @@ inline std::vector WStringToSQLWCHAR(const std::wstring& str) { #endif #if defined(__APPLE__) || defined(__linux__) -#include "pybind/unix_buffers.h" // For Unix-specific buffer handling -#include "pybind/unix_utils.h" // For Unix-specific Unicode encoding fixes +#include "unix_buffers.h" // For Unix-specific buffer handling +#include "unix_utils.h" // For Unix-specific Unicode encoding fixes // Unicode constants for surrogate ranges and max scalar value constexpr uint32_t UNICODE_SURROGATE_HIGH_START = 0xD800; diff --git a/mssql_python/pybind/unix_buffers.h b/mssql_python/pybind/unix_buffers.h index 38728da2..c339a73c 100644 --- a/mssql_python/pybind/unix_buffers.h +++ b/mssql_python/pybind/unix_buffers.h @@ -27,11 +27,11 @@ constexpr size_t UCS_LENGTH = 2; * handling memory allocation and conversion to std::wstring. */ class SQLWCHARBuffer { - private: + private: std::unique_ptr buffer; size_t buffer_size; - public: + public: /** * Constructor allocates a buffer of the specified size */ @@ -85,7 +85,7 @@ class SQLWCHARBuffer { * Similar to the error list handling in the Python PoC _check_ret function */ class DiagnosticRecords { - private: + private: struct Record { std::wstring sqlState; std::wstring message; @@ -94,7 +94,7 @@ class DiagnosticRecords { std::vector records; - public: + public: void addRecord(const std::wstring& sqlState, const std::wstring& message, SQLINTEGER nativeError) { records.push_back({sqlState, message, nativeError}); @@ -125,7 +125,9 @@ class DiagnosticRecords { // Add additional error messages if there are any for (size_t i = 1; i < records.size(); i++) { - fullMessage += L"; [" + records[i].sqlState + L"] " + records[i].message; + fullMessage += L"; [" + + records[i].sqlState + L"] " + + records[i].message; } return fullMessage; diff --git a/mssql_python/pybind/unix_utils.cpp b/mssql_python/pybind/unix_utils.cpp index 60665d8f..36e1c689 100644 --- a/mssql_python/pybind/unix_utils.cpp +++ b/mssql_python/pybind/unix_utils.cpp @@ -6,6 +6,11 @@ // between SQLWCHAR, std::wstring, and UTF-8 strings to bridge encoding // differences specific to macOS. +#include "unix_utils.h" +#include +#include +#include + #if defined(__APPLE__) || defined(__linux__) // Constants for character encoding const char* kOdbcEncoding = "utf-16-le"; // ODBC uses UTF-16LE for SQLWCHAR From d90e48f3dcce9d0f3938880d2780ab5f97b26d2c Mon Sep 17 00:00:00 2001 From: Jahnvi Thakkar Date: Tue, 9 Sep 2025 12:13:58 +0530 Subject: [PATCH 07/20] Resolving errors --- .../pybind/connection/connection_pool.h | 6 ++- mssql_python/pybind/unix_utils.cpp | 50 +++++++++++++------ 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/mssql_python/pybind/connection/connection_pool.h b/mssql_python/pybind/connection/connection_pool.h index 7a7a7b2e..1503bf8a 100644 --- a/mssql_python/pybind/connection/connection_pool.h +++ b/mssql_python/pybind/connection/connection_pool.h @@ -1,7 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -// INFO|TODO - Note that is file is Windows specific right now. Making it arch agnostic will be +// INFO|TODO - Note that is file is Windows specific right now. +// Making it arch agnostic will be // taken up in future. #pragma once @@ -33,7 +34,8 @@ class ConnectionPool { private: size_t _max_size; // Maximum number of connections allowed - int _idle_timeout_secs; // Idle time before connections are considered stale + // Idle time before connections are considered stale + int _idle_timeout_secs; size_t _current_size = 0; std::deque> _pool; // Available connections std::mutex _mutex; // Mutex for thread-safe access diff --git a/mssql_python/pybind/unix_utils.cpp b/mssql_python/pybind/unix_utils.cpp index 36e1c689..b7a85ad5 100644 --- a/mssql_python/pybind/unix_utils.cpp +++ b/mssql_python/pybind/unix_utils.cpp @@ -13,15 +13,19 @@ #if defined(__APPLE__) || defined(__linux__) // Constants for character encoding -const char* kOdbcEncoding = "utf-16-le"; // ODBC uses UTF-16LE for SQLWCHAR -const size_t kUcsLength = 2; // SQLWCHAR is 2 bytes on all platforms +// ODBC uses UTF-16LE for SQLWCHAR +const char* kOdbcEncoding = "utf-16-le"; +// SQLWCHAR is 2 bytes on all platforms +const size_t kUcsLength = 2; // TODO: Make Logger a separate module and import it across the project template void LOG(const std::string& formatString, Args&&... args) { py::gil_scoped_acquire gil; // <---- this ensures safe Python API usage - py::object logger = py::module_::import("mssql_python.logging_config").attr("get_logger")(); + py::object logger = + py::module_::import( + "mssql_python.logging_config").attr("get_logger")(); if (py::isinstance(logger)) return; try { @@ -29,7 +33,9 @@ void LOG(const std::string& formatString, Args&&... args) { if constexpr (sizeof...(args) == 0) { logger.attr("debug")(py::str(ddbcFormatString)); } else { - py::str message = py::str(ddbcFormatString).format(std::forward(args)...); + py::str message = + py::str(ddbcFormatString).format( + std::forward(args)...); logger.attr("debug")(message); } } catch (const std::exception& e) { @@ -38,7 +44,9 @@ void LOG(const std::string& formatString, Args&&... args) { } // Function to convert SQLWCHAR strings to std::wstring on macOS -std::wstring SQLWCHARToWString(const SQLWCHAR* sqlwStr, size_t length = SQL_NTS) { +std::wstring SQLWCHARToWString( + const SQLWCHAR* sqlwStr, + size_t length = SQL_NTS) { if (!sqlwStr) return std::wstring(); if (length == SQL_NTS) { @@ -58,14 +66,19 @@ std::wstring SQLWCHARToWString(const SQLWCHAR* sqlwStr, size_t length = SQL_NTS) // Convert UTF-16LE to std::wstring (UTF-32 on macOS) try { // Use C++11 codecvt to convert between UTF-16LE and wstring - std::wstring_convert> + std::wstring_convert< + std::codecvt_utf8_utf16< + wchar_t, 0x10ffff, std::little_endian>> converter; return converter.from_bytes( reinterpret_cast(utf16Bytes.data()), - reinterpret_cast(utf16Bytes.data() + utf16Bytes.size())); + reinterpret_cast( + utf16Bytes.data() + utf16Bytes.size())); } catch (const std::exception& e) { // Log a warning about using fallback conversion - LOG("Warning: Using fallback string conversion on macOS. Character data might be inexact."); + LOG( + "Warning: Using fallback string conversion on macOS." + " Character data might be inexact."); // Fallback to character-by-character conversion if codecvt fails std::wstring result; result.reserve(length); @@ -80,7 +93,10 @@ std::wstring SQLWCHARToWString(const SQLWCHAR* sqlwStr, size_t length = SQL_NTS) std::vector WStringToSQLWCHAR(const std::wstring& str) { try { // Convert wstring (UTF-32 on macOS) to UTF-16LE bytes - std::wstring_convert> + std::wstring_convert< + std::codecvt_utf8_utf16< + wchar_t, 0x10ffff, + std::little_endian>> converter; std::string utf16Bytes = converter.to_bytes(str); @@ -93,10 +109,12 @@ std::vector WStringToSQLWCHAR(const std::wstring& str) { return result; } catch (const std::exception& e) { // Log a warning about using fallback conversion - LOG("Warning: Using fallback conversion for std::wstring to SQLWCHAR on macOS. Character " + LOG("Warning: Using fallback conversion for" + " std::wstring to SQLWCHAR on macOS. Character " "data might be inexact."); // Fallback to simple casting if codecvt fails - std::vector result(str.size() + 1, 0); // +1 for null terminator + // +1 for null terminator + std::vector result(str.size() + 1, 0); for (size_t i = 0; i < str.size(); ++i) { result[i] = static_cast(str[i]); } @@ -120,14 +138,18 @@ std::string SQLWCHARToUTF8String(const SQLWCHAR* buffer) { } try { - std::wstring_convert> + std::wstring_convert< + std::codecvt_utf8_utf16< + char16_t, 0x10ffff, std::little_endian>> converter; return converter.to_bytes( reinterpret_cast(utf16Bytes.data()), - reinterpret_cast(utf16Bytes.data() + utf16Bytes.size())); + reinterpret_cast( + utf16Bytes.data() + utf16Bytes.size())); } catch (const std::exception& e) { // Log a warning about using fallback conversion - LOG("Warning: Using fallback conversion for SQLWCHAR to UTF-8 on macOS. Character data " + LOG("Warning: Using fallback conversion for " + "SQLWCHAR to UTF-8 on macOS. Character data " "might be inexact."); // Simple fallback conversion std::string result; From f11235635d8a0f21f9989380026c70b7502695f6 Mon Sep 17 00:00:00 2001 From: Jahnvi Thakkar Date: Tue, 9 Sep 2025 12:29:17 +0530 Subject: [PATCH 08/20] Resolving errors --- mssql_python/pybind/ddbc_bindings.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mssql_python/pybind/ddbc_bindings.h b/mssql_python/pybind/ddbc_bindings.h index eae70d80..913d3d9d 100644 --- a/mssql_python/pybind/ddbc_bindings.h +++ b/mssql_python/pybind/ddbc_bindings.h @@ -339,8 +339,8 @@ typedef void* DriverHandle; #endif // Platform-agnostic function to get a function pointer from the loaded library -template -T GetFunctionPointer(DriverHandle handle, const char* functionName) { +template T GetFunctionPointer( + DriverHandle handle, const char* functionName) { #ifdef _WIN32 // Windows: Use GetProcAddress return reinterpret_cast(GetProcAddress(handle, functionName)); From 70f71709b8330d7a5ac6cad8540213f70055ba59 Mon Sep 17 00:00:00 2001 From: Jahnvi Thakkar Date: Tue, 9 Sep 2025 12:36:07 +0530 Subject: [PATCH 09/20] Resolving errors --- mssql_python/pybind/unix_utils.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/mssql_python/pybind/unix_utils.cpp b/mssql_python/pybind/unix_utils.cpp index b7a85ad5..aaa6e17f 100644 --- a/mssql_python/pybind/unix_utils.cpp +++ b/mssql_python/pybind/unix_utils.cpp @@ -6,7 +6,6 @@ // between SQLWCHAR, std::wstring, and UTF-8 strings to bridge encoding // differences specific to macOS. -#include "unix_utils.h" #include #include #include From 901c30dc7d4c12685a981421d3436606cce955d7 Mon Sep 17 00:00:00 2001 From: Jahnvi Thakkar Date: Tue, 9 Sep 2025 12:38:55 +0530 Subject: [PATCH 10/20] Resolving errors --- mssql_python/pybind/ddbc_bindings.h | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/mssql_python/pybind/ddbc_bindings.h b/mssql_python/pybind/ddbc_bindings.h index 913d3d9d..6499fa30 100644 --- a/mssql_python/pybind/ddbc_bindings.h +++ b/mssql_python/pybind/ddbc_bindings.h @@ -21,13 +21,13 @@ using pybind11::literals::operator""_a; #include #ifdef _WIN32 -// Windows-specific headers -#include // windows.h needs to be included before sql.h -#include -#pragma comment(lib, "shlwapi.lib") -#define IS_WINDOWS 1 + // Windows-specific headers + #include // windows.h needs to be included before sql.h + #include + #pragma comment(lib, "shlwapi.lib") + #define IS_WINDOWS 1 #else -#define IS_WINDOWS 0 + #define IS_WINDOWS 0 #endif #include @@ -42,8 +42,9 @@ inline std::vector WStringToSQLWCHAR(const std::wstring& str) { #endif #if defined(__APPLE__) || defined(__linux__) -#include "unix_buffers.h" // For Unix-specific buffer handling -#include "unix_utils.h" // For Unix-specific Unicode encoding fixes + #include // For dlsym, dlopen, and other dynamic linking functions + #include "unix_buffers.h" // For Unix-specific buffer handling + #include "unix_utils.h" // For Unix-specific Unicode encoding fixes // Unicode constants for surrogate ranges and max scalar value constexpr uint32_t UNICODE_SURROGATE_HIGH_START = 0xD800; From c5697c923449a6a0945b10413a5aaebb6a3c4c02 Mon Sep 17 00:00:00 2001 From: Jahnvi Thakkar Date: Wed, 10 Sep 2025 09:32:38 +0530 Subject: [PATCH 11/20] Resolving conflicts --- mssql_python/msvcp140.dll | Bin 575592 -> 0 bytes mssql_python/pybind/ddbc_bindings.cpp | 4 +++- 2 files changed, 3 insertions(+), 1 deletion(-) delete mode 100644 mssql_python/msvcp140.dll diff --git a/mssql_python/msvcp140.dll b/mssql_python/msvcp140.dll deleted file mode 100644 index 0a9b13d7585510f6f52e6c3649e26236d1e77c55..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 575592 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

jwKIraloi_ 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⁢OlVtmx4cX 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 duVT!w=AD>`VqBYw5Xqe8!9rMTTIgmg_ Date: Wed, 10 Sep 2025 10:01:18 +0530 Subject: [PATCH 12/20] Adding fully types methods for all the methods in mssql_python --- mssql_python/connection.py | 8 ++-- mssql_python/cursor.py | 70 +++++++++++++++++----------------- mssql_python/exceptions.py | 22 +++++------ mssql_python/helpers.py | 6 +-- mssql_python/logging_config.py | 18 ++++----- mssql_python/pooling.py | 8 ++-- mssql_python/row.py | 16 ++++---- mssql_python/type.py | 14 +++---- 8 files changed, 81 insertions(+), 81 deletions(-) diff --git a/mssql_python/connection.py b/mssql_python/connection.py index c92dc08e..348787c5 100644 --- a/mssql_python/connection.py +++ b/mssql_python/connection.py @@ -276,7 +276,7 @@ def setautocommit(self, value: bool = False) -> None: """ self._conn.set_autocommit(value) - def setencoding(self, encoding=None, ctype=None): + def setencoding(self, encoding: str = None, ctype: int = None) -> None: """ Sets the text encoding for SQL statements and text parameters. @@ -389,7 +389,7 @@ def getencoding(self): return self._encoding_settings.copy() - def setdecoding(self, sqltype, encoding=None, ctype=None): + def setdecoding(self, sqltype: int, encoding: str = None, ctype: int = None) -> None: """ Sets the text decoding used when reading SQL_CHAR and SQL_WCHAR from the database. @@ -515,7 +515,7 @@ def setdecoding(self, sqltype, encoding=None, ctype=None): sanitize_user_input(str(ctype)), ) - def getdecoding(self, sqltype): + def getdecoding(self, sqltype: int) -> dict: """ Gets the current text decoding settings for the specified SQL type. @@ -714,7 +714,7 @@ def __exit__(self, *args) -> None: if not self._closed: self.close() - def __del__(self): + def __del__(self) -> None: """ Destructor to ensure the connection is closed when the connection object is no longer needed. diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index 27c37552..f0d878f2 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -14,7 +14,7 @@ import decimal import uuid import datetime -from typing import List, Union +from typing import List, Union, Any, Optional from mssql_python.constants import ConstantsDDBC as ddbc_sql_const from mssql_python.helpers import check_error, log from mssql_python import ddbc_bindings @@ -97,7 +97,7 @@ def __init__(self, connection) -> None: self.messages = [] # Store diagnostic messages - def _is_unicode_string(self, param): + def _is_unicode_string(self, param: str) -> bool: """ Check if a string contains non-ASCII characters. @@ -113,7 +113,7 @@ def _is_unicode_string(self, param): except UnicodeEncodeError: return True # Contains non-ASCII characters, so treat as Unicode - def _parse_date(self, param): + def _parse_date(self, param: str) -> Union[datetime.date, None]: """ Attempt to parse a string as a date. @@ -131,7 +131,7 @@ def _parse_date(self, param): continue return None - def _parse_datetime(self, param): + def _parse_datetime(self, param: str) -> Union[datetime.datetime, None]: """ Attempt to parse a string as a datetime, smalldatetime, datetime2, timestamp. @@ -155,7 +155,7 @@ def _parse_datetime(self, param): return None # If all formats fail, return None - def _parse_time(self, param): + def _parse_time(self, param: str) -> Union[datetime.time, None]: """ Attempt to parse a string as a time. @@ -176,7 +176,7 @@ def _parse_time(self, param): continue return None - def _get_numeric_data(self, param): + def _get_numeric_data(self, param: decimal.Decimal) -> ddbc_bindings.NumericData: """ Get the data for a numeric parameter. @@ -230,7 +230,7 @@ def _get_numeric_data(self, param): numeric_data.val = val return numeric_data - def _map_sql_type(self, param, parameters_list, i): + def _map_sql_type(self, param: Any, parameters_list: list, i: int) -> tuple: """ Map a Python data type to the corresponding SQL type, C type, Column size, and Decimal digits. @@ -457,7 +457,7 @@ def _initialize_cursor(self) -> None: """ self._allocate_statement_handle() - def _allocate_statement_handle(self): + def _allocate_statement_handle(self) -> None: """ Allocate the DDBC statement handle. """ @@ -500,7 +500,7 @@ def close(self) -> None: self._clear_rownumber() self.closed = True - def _check_closed(self): + def _check_closed(self) -> None: """ Check if the cursor is closed and raise an exception if it is. @@ -513,7 +513,7 @@ def _check_closed(self): ddbc_error="" ) - def _create_parameter_types_list(self, parameter, param_info, parameters_list, i): + def _create_parameter_types_list(self, parameter: Any, param_info: Any, parameters_list: list, i: int) -> None: """ Maps parameter types for the given parameter. @@ -539,7 +539,7 @@ def _create_parameter_types_list(self, parameter, param_info, parameters_list, i return paraminfo - def _initialize_description(self): + def _initialize_description(self) -> None: """ Initialize the description attribute using SQLDescribeCol. """ @@ -560,7 +560,7 @@ def _initialize_description(self): for col in col_metadata ] - def _map_data_type(self, sql_type): + def _map_data_type(self, sql_type: int) -> type: """ Map SQL data type to Python data type. @@ -596,7 +596,7 @@ def _map_data_type(self, sql_type): return sql_to_python_type.get(sql_type, str) @property - def rownumber(self): + def rownumber(self) -> Union[int, None]: """ DB-API extension: Current 0-based index of the cursor in the result set. @@ -623,7 +623,7 @@ def rownumber(self): return self._rownumber # Will be None until first fetch, then 0, 1, 2, etc. @property - def connection(self): + def connection(self) -> "Connection": """ DB-API 2.0 attribute: Connection object that created this cursor. @@ -640,14 +640,14 @@ def connection(self): """ return self._connection - def _reset_rownumber(self): + def _reset_rownumber(self) -> None: """Reset the rownumber tracking when starting a new result set.""" self._rownumber = -1 self._next_row_index = 0 self._has_result_set = True self._skip_increment_for_next_fetch = False - def _increment_rownumber(self): + def _increment_rownumber(self) -> None: """ Called after a successful fetch from the driver. Keep both counters consistent. """ @@ -663,7 +663,7 @@ def _increment_rownumber(self): ) # Will be used when we add support for scrollable cursors - def _decrement_rownumber(self): + def _decrement_rownumber(self) -> None: """ Decrement the rownumber by 1. @@ -680,7 +680,7 @@ def _decrement_rownumber(self): "No active result set.", ) - def _clear_rownumber(self): + def _clear_rownumber(self) -> None: """ Clear the rownumber tracking. @@ -690,7 +690,7 @@ def _clear_rownumber(self): self._has_result_set = False self._skip_increment_for_next_fetch = False - def __iter__(self): + def __iter__(self) -> "Cursor": """ Return the cursor itself as an iterator. @@ -702,7 +702,7 @@ def __iter__(self): self._check_closed() return self - def __next__(self): + def __next__(self) -> "Row": """ Fetch the next row when iterating over the cursor. @@ -718,7 +718,7 @@ def __next__(self): raise StopIteration return row - def next(self): + def next(self) -> "Row": """ Fetch the next row from the cursor. @@ -838,7 +838,7 @@ def execute( return self @staticmethod - def _select_best_sample_value(column): + def _select_best_sample_value(column: List[Any]) -> Any: """ Selects the most representative non-null value from a column for type inference. @@ -1102,7 +1102,7 @@ def nextset(self) -> Union[bool, None]: return True - def __enter__(self): + def __enter__(self) -> "Cursor": """ Enter the runtime context for the cursor. @@ -1112,13 +1112,13 @@ def __enter__(self): self._check_closed() return self - def __exit__(self, *args): + def __exit__(self, *args) -> None: """Closes the cursor when exiting the context, ensuring proper resource cleanup.""" if not self.closed: self.close() return None - def fetchval(self): + def fetchval(self) -> Any: """ Fetch the first column of the first row if there are results. @@ -1154,7 +1154,7 @@ def fetchval(self): return None if row is None else row[0] - def commit(self): + def commit(self) -> None: """ Commit all SQL statements executed on the connection that created this cursor. @@ -1183,7 +1183,7 @@ def commit(self): # Delegate to the connection's commit method self._connection.commit() - def rollback(self): + def rollback(self) -> None: """ Roll back all SQL statements executed on the connection that created this cursor. @@ -1212,7 +1212,7 @@ def rollback(self): # Delegate to the connection's rollback method self._connection.rollback() - def __del__(self): + def __del__(self) -> None: """ Destructor to ensure the cursor is closed when it is no longer needed. This is a safety net to ensure resources are cleaned up @@ -1351,12 +1351,12 @@ def skip(self, count: int) -> None: def _execute_tables( self, - stmt_handle, - catalog_name=None, - schema_name=None, - table_name=None, - table_type=None - ): + stmt_handle: int, + catalog_name: Optional[str] = None, + schema_name: Optional[str] = None, + table_name: Optional[str] = None, + table_type: Optional[str] = None + ) -> None: """ Execute SQLTables ODBC function to retrieve table metadata. @@ -1385,7 +1385,7 @@ def _execute_tables( if stmt_handle: self.messages.extend(ddbc_bindings.DDBCSQLGetAllDiagRecords(stmt_handle)) - def tables(self, table=None, catalog=None, schema=None, table_type=None): + def tables(self, table: Optional[str] = None, catalog: Optional[str] = None, schema: Optional[str] = None, table_type: Optional[Union[str, List[str]]] = None) -> None: """ Returns information about tables in the database that match the given criteria using the SQLTables ODBC function. diff --git a/mssql_python/exceptions.py b/mssql_python/exceptions.py index d0069c60..4a2440dd 100644 --- a/mssql_python/exceptions.py +++ b/mssql_python/exceptions.py @@ -15,7 +15,7 @@ class Exception(Exception): Base class for all DB API 2.0 exceptions. """ - def __init__(self, driver_error, ddbc_error) -> None: + def __init__(self, driver_error: str, ddbc_error: str) -> None: self.driver_error = driver_error self.ddbc_error = truncate_error_message(ddbc_error) if self.ddbc_error: @@ -34,7 +34,7 @@ class Warning(Exception): Exception raised for important warnings like data truncations while inserting, etc. """ - def __init__(self, driver_error, ddbc_error) -> None: + def __init__(self, driver_error: str, ddbc_error: str) -> None: super().__init__(driver_error, ddbc_error) @@ -43,7 +43,7 @@ class Error(Exception): Base class for errors. """ - def __init__(self, driver_error, ddbc_error) -> None: + def __init__(self, driver_error: str, ddbc_error: str) -> None: super().__init__(driver_error, ddbc_error) @@ -53,7 +53,7 @@ class InterfaceError(Error): interface rather than the database itself. """ - def __init__(self, driver_error, ddbc_error) -> None: + def __init__(self, driver_error: str, ddbc_error: str) -> None: super().__init__(driver_error, ddbc_error) @@ -62,7 +62,7 @@ class DatabaseError(Error): Exception raised for errors that are related to the database. """ - def __init__(self, driver_error, ddbc_error) -> None: + def __init__(self, driver_error: str, ddbc_error: str) -> None: super().__init__(driver_error, ddbc_error) @@ -72,7 +72,7 @@ class DataError(DatabaseError): processed data like division by zero, numeric value out of range, etc. """ - def __init__(self, driver_error, ddbc_error) -> None: + def __init__(self, driver_error: str, ddbc_error: str) -> None: super().__init__(driver_error, ddbc_error) @@ -82,7 +82,7 @@ class OperationalError(DatabaseError): and not necessarily under the control of the programmer. """ - def __init__(self, driver_error, ddbc_error) -> None: + def __init__(self, driver_error: str, ddbc_error: str) -> None: super().__init__(driver_error, ddbc_error) @@ -92,7 +92,7 @@ class IntegrityError(DatabaseError): e.g., a foreign key check fails. """ - def __init__(self, driver_error, ddbc_error) -> None: + def __init__(self, driver_error: str, ddbc_error: str) -> None: super().__init__(driver_error, ddbc_error) @@ -102,7 +102,7 @@ class InternalError(DatabaseError): e.g., the cursor is not valid anymore, the transaction is out of sync, etc. """ - def __init__(self, driver_error, ddbc_error) -> None: + def __init__(self, driver_error: str, ddbc_error: str) -> None: super().__init__(driver_error, ddbc_error) @@ -113,7 +113,7 @@ class ProgrammingError(DatabaseError): wrong number of parameters specified, etc. """ - def __init__(self, driver_error, ddbc_error) -> None: + def __init__(self, driver_error: str, ddbc_error: str) -> None: super().__init__(driver_error, ddbc_error) @@ -124,7 +124,7 @@ class NotSupportedError(DatabaseError): on a connection that does not support transaction or has transactions turned off. """ - def __init__(self, driver_error, ddbc_error) -> None: + def __init__(self, driver_error: str, ddbc_error: str) -> None: super().__init__(driver_error, ddbc_error) diff --git a/mssql_python/helpers.py b/mssql_python/helpers.py index 4c3b60b4..d68b4209 100644 --- a/mssql_python/helpers.py +++ b/mssql_python/helpers.py @@ -12,7 +12,7 @@ logger = get_logger() -def add_driver_to_connection_str(connection_str): +def add_driver_to_connection_str(connection_str: str) -> str: """ Add the DDBC driver to the connection string if not present. @@ -57,7 +57,7 @@ def add_driver_to_connection_str(connection_str): return connection_str -def check_error(handle_type, handle, ret): +def check_error(handle_type: int, handle: int, ret: int) -> None: """ Check for errors and raise an exception if an error is found. @@ -76,7 +76,7 @@ def check_error(handle_type, handle, ret): raise_exception(error_info.sqlState, error_info.ddbcErrorMsg) -def add_driver_name_to_app_parameter(connection_string): +def add_driver_name_to_app_parameter(connection_string: str) -> str: """ Modifies the input connection string by appending the APP name. diff --git a/mssql_python/logging_config.py b/mssql_python/logging_config.py index 567127c0..2119f191 100644 --- a/mssql_python/logging_config.py +++ b/mssql_python/logging_config.py @@ -23,34 +23,34 @@ class LoggingManager: _logger = None _log_file = None - def __new__(cls): + def __new__(cls) -> "LoggingManager": if cls._instance is None: cls._instance = super(LoggingManager, cls).__new__(cls) return cls._instance - def __init__(self): + def __init__(self) -> None: if not self._initialized: self._initialized = True self._enabled = False @classmethod - def is_logging_enabled(cls): + def is_logging_enabled(cls) -> bool: """Class method to check if logging is enabled for backward compatibility""" if cls._instance is None: return False return cls._instance._enabled @property - def enabled(self): + def enabled(self) -> bool: """Check if logging is enabled""" return self._enabled @property - def log_file(self): + def log_file(self) -> str: """Get the current log file path""" return self._log_file - def setup(self, mode="file", log_level=logging.DEBUG): + def setup(self, mode: str = "file", log_level: int = logging.DEBUG) -> None: """ Set up logging configuration. @@ -105,7 +105,7 @@ class PythonLayerFormatter(logging.Formatter): """ Custom formatter for Python Layer logs. """ - def format(self, record): + def format(self, record: logging.LogRecord) -> str: message = record.getMessage() # Don't add [Python Layer log] prefix if the message already has # [DDBC Bindings log] or [Python Layer log] @@ -155,7 +155,7 @@ def get_logger(self): _manager = LoggingManager() -def setup_logging(mode="file", log_level=logging.DEBUG): +def setup_logging(mode: str = "file", log_level: int = logging.DEBUG) -> None: """ Set up logging configuration. @@ -168,7 +168,7 @@ def setup_logging(mode="file", log_level=logging.DEBUG): return _manager.setup(mode, log_level) -def get_logger(): +def get_logger() -> logging.Logger: """ Get the logger instance. diff --git a/mssql_python/pooling.py b/mssql_python/pooling.py index 551ef22f..485cccaa 100644 --- a/mssql_python/pooling.py +++ b/mssql_python/pooling.py @@ -20,7 +20,7 @@ class PoolingManager: _config = {"max_size": 100, "idle_timeout": 600} @classmethod - def enable(cls, max_size=100, idle_timeout=600): + def enable(cls, max_size: int = 100, idle_timeout: int = 600) -> None: """ Enable connection pooling with the specified parameters. @@ -42,7 +42,7 @@ def enable(cls, max_size=100, idle_timeout=600): cls._initialized = True @classmethod - def disable(cls): + def disable(cls) -> None: """ Disable connection pooling. """ @@ -51,14 +51,14 @@ def disable(cls): cls._initialized = True @classmethod - def is_enabled(cls): + def is_enabled(cls) -> bool: """ Check if connection pooling is enabled. """ return cls._enabled @classmethod - def is_initialized(cls): + def is_initialized(cls) -> bool: """ Check if connection pooling is initialized. """ diff --git a/mssql_python/row.py b/mssql_python/row.py index 267cbe1c..70c2f7b5 100644 --- a/mssql_python/row.py +++ b/mssql_python/row.py @@ -15,7 +15,7 @@ class Row: print(row.column_name) # Access by column name """ - def __init__(self, values, description, column_map=None): + def __init__(self, values: list, description: list, column_map: dict = None) -> None: """ Initialize a Row object with values and description. @@ -39,11 +39,11 @@ def __init__(self, values, description, column_map=None): else: self._column_map = column_map - def __getitem__(self, index): + def __getitem__(self, index: int) -> any: """Allow accessing by numeric index: row[0]""" return self._values[index] - def __getattr__(self, name): + def __getattr__(self, name: str) -> any: """Allow accessing by column name as attribute: row.column_name""" if name in self._column_map: return self._values[self._column_map[name]] @@ -51,7 +51,7 @@ def __getattr__(self, name): f"'{self.__class__.__name__}' object has no attribute '{name}'" ) - def __eq__(self, other): + def __eq__(self, other: object) -> bool: """ Support comparison with lists for test compatibility. This is the key change needed to fix the tests. @@ -62,18 +62,18 @@ def __eq__(self, other): return self._values == other._values return super().__eq__(other) - def __len__(self): + def __len__(self) -> int: """Return the number of values in the row""" return len(self._values) - def __iter__(self): + def __iter__(self) -> iter: """Allow iteration through values""" return iter(self._values) - def __str__(self): + def __str__(self) -> str: """Return string representation of the row""" return str(tuple(self._values)) - def __repr__(self): + def __repr__(self) -> str: """Return a detailed string representation for debugging""" return repr(tuple(self._values)) diff --git a/mssql_python/type.py b/mssql_python/type.py index 7963631d..942c1863 100644 --- a/mssql_python/type.py +++ b/mssql_python/type.py @@ -6,7 +6,7 @@ import datetime import time - +from typing import Union # Type Objects class STRING(str): @@ -14,7 +14,7 @@ class STRING(str): This type object is used to describe columns in a database that are string-based (e.g. CHAR). """ - def __new__(cls): + def __new__(cls) -> str: return str.__new__(cls, "") @@ -24,7 +24,7 @@ class BINARY(bytearray): binary columns in a database (e.g. LONG, RAW, BLOBs). """ - def __new__(cls): + def __new__(cls) -> bytearray: return bytearray.__new__(cls) @@ -33,7 +33,7 @@ class NUMBER(float): This type object is used to describe numeric columns in a database. """ - def __new__(cls): + def __new__(cls) -> float: return float.__new__(cls, 0.0) @@ -42,7 +42,7 @@ class DATETIME(datetime.datetime): This type object is used to describe date/time columns in a database. """ - def __new__(cls, year: int = 1, month: int = 1, day: int = 1): + def __new__(cls, year: int = 1, month: int = 1, day: int = 1) -> datetime.datetime: return datetime.datetime.__new__(cls, year, month, day) @@ -51,7 +51,7 @@ class ROWID(int): This type object is used to describe the "Row ID" column in a database. """ - def __new__(cls): + def __new__(cls) -> int: return int.__new__(cls, 0) @@ -106,7 +106,7 @@ def TimestampFromTicks(ticks: int) -> datetime.datetime: return datetime.datetime.fromtimestamp(ticks) -def Binary(value) -> bytes: +def Binary(value: Union[str, bytes, bytearray]) -> bytes: """ Converts a string or bytes to bytes for use with binary database columns. From 99747af0282e02017a51487450dd8c8ceade310e Mon Sep 17 00:00:00 2001 From: Jahnvi Thakkar Date: Wed, 10 Sep 2025 11:52:48 +0530 Subject: [PATCH 13/20] Adding precommit hook as well as workflow --- .github/workflows/code-quality-check.yml | 126 +++++++++++++++++++++++ .github/workflows/devskim.yml | 3 - .pre-commit-config.yml | 16 +++ cpplint.cfg | 4 + pyproject.toml | 11 ++ 5 files changed, 157 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/code-quality-check.yml create mode 100644 .pre-commit-config.yml create mode 100644 cpplint.cfg create mode 100644 pyproject.toml diff --git a/.github/workflows/code-quality-check.yml b/.github/workflows/code-quality-check.yml new file mode 100644 index 00000000..d7856940 --- /dev/null +++ b/.github/workflows/code-quality-check.yml @@ -0,0 +1,126 @@ +name: Code Quality + +on: + pull_request: + types: [opened, edited, reopened, synchronize] + paths: + - '**.py' + - '**.cpp' + - '**.h' + +jobs: + lint: + name: Code Linting + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.9' + + - name: Cache pip dependencies + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e . + pip install pylint cpplint + + - name: Run Python Linter + id: pylint + continue-on-error: true + run: | + echo "Running pylint with threshold 8.5..." + python -m pylint --fail-under=8.5 --disable=fixme,no-member,too-many-arguments,too-many-positional-arguments,invalid-name,useless-parent-delegation --output-format=colorized mssql_python + echo "pylint_status=$?" >> $GITHUB_ENV + + - name: Run C++ Linter + id: cpplint + continue-on-error: true + run: | + echo "Running cpplint with maximum 10 errors per file..." + + # Find C++ files excluding build directories + FILES=$(find mssql_python -name "*.cpp" -o -name "*.h" | grep -v "/build/") + MAX_ERRORS=10 + FAILED_FILES="" + + # Run cpplint on all files and capture output + CPPLINT_OUTPUT=$(python -m cpplint --filter=-readability/todo --linelength=100 $FILES 2>&1) + echo "$CPPLINT_OUTPUT" + + # Extract error counts per file + while IFS= read -r line; do + if [[ $line =~ Total\ errors\ found:\ ([0-9]+) ]]; then + ERROR_COUNT=${BASH_REMATCH[1]} + + # Get the filename from the previous line + PREV_LINE=$(echo "$CPPLINT_OUTPUT" | grep -B 1 "$line" | head -n 1) + if [[ $PREV_LINE =~ Done\ processing\ (.+) ]]; then + FILE_PATH=${BASH_REMATCH[1]} + FILE_NAME=$(basename "$FILE_PATH") + + echo "File $FILE_NAME has $ERROR_COUNT errors" + + # Check if file exceeds threshold + if [ $ERROR_COUNT -gt $MAX_ERRORS ]; then + FAILED_FILES="$FAILED_FILES\n- $FILE_NAME ($ERROR_COUNT errors)" + fi + fi + fi + done <<< "$CPPLINT_OUTPUT" + + if [ ! -z "$FAILED_FILES" ]; then + echo -e "\n⛔ The following files have more than $MAX_ERRORS errors:$FAILED_FILES" + echo "cpplint_status=1" >> $GITHUB_ENV + else + echo -e "\n✅ All files have $MAX_ERRORS or fewer errors." + echo "cpplint_status=0" >> $GITHUB_ENV + fi + + - name: Determine overall status + run: | + if [[ "${{ env.pylint_status }}" != "0" || "${{ env.cpplint_status }}" != "0" ]]; then + echo "Linting checks failed!" + exit 1 + else + echo "All linting checks passed!" + fi + + - name: Comment on PR + if: github.event_name == 'pull_request' && (env.pylint_status != '0' || env.cpplint_status != '0') + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + let comment = '## Code Quality Check Results\n\n'; + + if ('${{ env.pylint_status }}' !== '0') { + comment += '⚠️ **Python linting failed** - Please check the [workflow logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details.\n\n'; + } else { + comment += '✅ **Python linting passed**\n\n'; + } + + if ('${{ env.cpplint_status }}' !== '0') { + comment += '⚠️ **C++ linting failed** - Some files exceed the maximum error threshold of 10.\n\n'; + } else { + comment += '✅ **C++ linting passed**\n\n'; + } + + comment += 'See [code quality guidelines](https://github.com/microsoft/mssql-python/blob/main/CONTRIBUTING.md) for more information.'; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: comment + }); \ No newline at end of file diff --git a/.github/workflows/devskim.yml b/.github/workflows/devskim.yml index 9a71f870..86f469d4 100644 --- a/.github/workflows/devskim.yml +++ b/.github/workflows/devskim.yml @@ -32,6 +32,3 @@ jobs: uses: github/codeql-action/upload-sarif@v3 with: sarif_file: devskim-results.sarif - # Exclude TODO (DS176209) and localhost (DS162092) alerts - # Since they are used only for future improvements & tests respectively - exclude-rules: DS176209,DS162092 diff --git a/.pre-commit-config.yml b/.pre-commit-config.yml new file mode 100644 index 00000000..ba123c45 --- /dev/null +++ b/.pre-commit-config.yml @@ -0,0 +1,16 @@ +repos: +- repo: https://github.com/pre-commit/mirrors-pylint + rev: v3.0.0a5 + hooks: + - id: pylint + args: [--fail-under=8.5, --disable=fixme,no-member,too-many-arguments,too-many-positional-arguments,invalid-name,useless-parent-delegation] + +- repo: local + hooks: + - id: cpplint + name: cpplint + entry: python -m cpplint + language: python + types: [c++] + args: [--filter=-readability/todo, --linelength=100] + exclude: ^.*build/.*$ \ No newline at end of file diff --git a/cpplint.cfg b/cpplint.cfg new file mode 100644 index 00000000..d6a1e9b1 --- /dev/null +++ b/cpplint.cfg @@ -0,0 +1,4 @@ +set noparent +filter=-readability/todo +linelength=100 +exclude_files=build \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..4097a2e5 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,11 @@ +[tool.pylint] +disable = [ + "fixme", + "no-member", + "too-many-arguments", + "too-many-positional-arguments", + "invalid-name", + "useless-parent-delegation" +] +fail-under = 8.5 +max-line-length = 100 \ No newline at end of file From 34a386fe084153321dbaf1383edac06c877c4870 Mon Sep 17 00:00:00 2001 From: Jahnvi Thakkar Date: Wed, 10 Sep 2025 11:55:13 +0530 Subject: [PATCH 14/20] Adding precommit hook as well as workflow --- .github/workflows/code-quality-check.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/code-quality-check.yml b/.github/workflows/code-quality-check.yml index d7856940..db995538 100644 --- a/.github/workflows/code-quality-check.yml +++ b/.github/workflows/code-quality-check.yml @@ -1,5 +1,9 @@ name: Code Quality +permissions: + contents: read + issues: write + on: pull_request: types: [opened, edited, reopened, synchronize] From c81197d52d48263193ce86204824c7cbd84f1eea Mon Sep 17 00:00:00 2001 From: Jahnvi Thakkar Date: Wed, 10 Sep 2025 11:59:41 +0530 Subject: [PATCH 15/20] Adding precommit hook as well as workflow --- .github/workflows/code-quality-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code-quality-check.yml b/.github/workflows/code-quality-check.yml index db995538..dded8c1b 100644 --- a/.github/workflows/code-quality-check.yml +++ b/.github/workflows/code-quality-check.yml @@ -23,7 +23,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.9' + python-version: '3.11' - name: Cache pip dependencies uses: actions/cache@v3 From 4130cca9159cfbdd1b2821a0da148d0d9077c90e Mon Sep 17 00:00:00 2001 From: Jahnvi Thakkar Date: Wed, 10 Sep 2025 12:04:47 +0530 Subject: [PATCH 16/20] Adding precommit hook as well as workflow --- .github/workflows/code-quality-check.yml | 56 ++++++++++++++---------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/.github/workflows/code-quality-check.yml b/.github/workflows/code-quality-check.yml index dded8c1b..4fa58a2d 100644 --- a/.github/workflows/code-quality-check.yml +++ b/.github/workflows/code-quality-check.yml @@ -53,36 +53,46 @@ jobs: run: | echo "Running cpplint with maximum 10 errors per file..." - # Find C++ files excluding build directories + # Debug: Show what files we're finding + echo "Finding C++ files (excluding build directories)..." FILES=$(find mssql_python -name "*.cpp" -o -name "*.h" | grep -v "/build/") + if [ -z "$FILES" ]; then + echo "No C++ files found to check!" + echo "cpplint_status=0" >> $GITHUB_ENV + exit 0 + fi + + echo "Found files to check:" + echo "$FILES" + + # Run cpplint directly on files without complex parsing + echo "Running initial cpplint check..." + python -m cpplint --filter=-readability/todo --linelength=100 $FILES > cpplint_output.txt 2>&1 + CPPLINT_EXIT_CODE=$? + + # Display the output + cat cpplint_output.txt + + # Simple check for files with more than MAX_ERRORS errors MAX_ERRORS=10 FAILED_FILES="" - # Run cpplint on all files and capture output - CPPLINT_OUTPUT=$(python -m cpplint --filter=-readability/todo --linelength=100 $FILES 2>&1) - echo "$CPPLINT_OUTPUT" - - # Extract error counts per file + # Process the output line by line to find error counts + grep "Total errors found:" cpplint_output.txt > error_counts.txt while IFS= read -r line; do - if [[ $line =~ Total\ errors\ found:\ ([0-9]+) ]]; then - ERROR_COUNT=${BASH_REMATCH[1]} - - # Get the filename from the previous line - PREV_LINE=$(echo "$CPPLINT_OUTPUT" | grep -B 1 "$line" | head -n 1) - if [[ $PREV_LINE =~ Done\ processing\ (.+) ]]; then - FILE_PATH=${BASH_REMATCH[1]} - FILE_NAME=$(basename "$FILE_PATH") - - echo "File $FILE_NAME has $ERROR_COUNT errors" - - # Check if file exceeds threshold - if [ $ERROR_COUNT -gt $MAX_ERRORS ]; then - FAILED_FILES="$FAILED_FILES\n- $FILE_NAME ($ERROR_COUNT errors)" - fi - fi + # Extract error count and file name + ERROR_COUNT=$(echo "$line" | grep -o '[0-9]\+') + FILE_NAME=$(grep -B 1 "$line" cpplint_output.txt | head -n 1 | sed 's/Done processing //') + + echo "File $FILE_NAME has $ERROR_COUNT errors" + + # Check if over threshold + if [ "$ERROR_COUNT" -gt "$MAX_ERRORS" ]; then + FAILED_FILES="$FAILED_FILES\n- $FILE_NAME ($ERROR_COUNT errors)" fi - done <<< "$CPPLINT_OUTPUT" + done < error_counts.txt + # Output results if [ ! -z "$FAILED_FILES" ]; then echo -e "\n⛔ The following files have more than $MAX_ERRORS errors:$FAILED_FILES" echo "cpplint_status=1" >> $GITHUB_ENV From 4d21df076a51735312992fce9cb6405aaac1f0a1 Mon Sep 17 00:00:00 2001 From: Jahnvi Thakkar Date: Wed, 10 Sep 2025 12:08:43 +0530 Subject: [PATCH 17/20] Adding precommit hook as well as workflow --- .github/workflows/code-quality-check.yml | 43 ++++++++++++------------ 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/.github/workflows/code-quality-check.yml b/.github/workflows/code-quality-check.yml index 4fa58a2d..41a2d032 100644 --- a/.github/workflows/code-quality-check.yml +++ b/.github/workflows/code-quality-check.yml @@ -53,8 +53,7 @@ jobs: run: | echo "Running cpplint with maximum 10 errors per file..." - # Debug: Show what files we're finding - echo "Finding C++ files (excluding build directories)..." + # Find C++ files excluding build directories FILES=$(find mssql_python -name "*.cpp" -o -name "*.h" | grep -v "/build/") if [ -z "$FILES" ]; then echo "No C++ files found to check!" @@ -65,32 +64,32 @@ jobs: echo "Found files to check:" echo "$FILES" - # Run cpplint directly on files without complex parsing - echo "Running initial cpplint check..." - python -m cpplint --filter=-readability/todo --linelength=100 $FILES > cpplint_output.txt 2>&1 - CPPLINT_EXIT_CODE=$? - - # Display the output - cat cpplint_output.txt - - # Simple check for files with more than MAX_ERRORS errors + # Process each file individually to better handle errors MAX_ERRORS=10 FAILED_FILES="" - # Process the output line by line to find error counts - grep "Total errors found:" cpplint_output.txt > error_counts.txt - while IFS= read -r line; do - # Extract error count and file name - ERROR_COUNT=$(echo "$line" | grep -o '[0-9]\+') - FILE_NAME=$(grep -B 1 "$line" cpplint_output.txt | head -n 1 | sed 's/Done processing //') + for FILE in $FILES; do + echo "Checking $FILE..." + + # Run cpplint on a single file and capture output + OUTPUT=$(python -m cpplint --filter=-readability/todo --linelength=100 "$FILE" 2>&1) - echo "File $FILE_NAME has $ERROR_COUNT errors" + # Display the output for this file + echo "$OUTPUT" - # Check if over threshold - if [ "$ERROR_COUNT" -gt "$MAX_ERRORS" ]; then - FAILED_FILES="$FAILED_FILES\n- $FILE_NAME ($ERROR_COUNT errors)" + # Look for "Total errors found: X" in the output + if [[ "$OUTPUT" =~ "Total errors found: "([0-9]+) ]]; then + ERROR_COUNT="${BASH_REMATCH[1]}" + echo "File $FILE has $ERROR_COUNT errors" + + # Check if over threshold + if [ "$ERROR_COUNT" -gt "$MAX_ERRORS" ]; then + FAILED_FILES="$FAILED_FILES\n- $FILE ($ERROR_COUNT errors)" + fi + else + echo "Warning: Could not determine error count for $FILE" fi - done < error_counts.txt + done # Output results if [ ! -z "$FAILED_FILES" ]; then From 8966cae9a0ae0593c2206f561dab61d35a3f4f80 Mon Sep 17 00:00:00 2001 From: Jahnvi Thakkar Date: Wed, 10 Sep 2025 12:11:23 +0530 Subject: [PATCH 18/20] Adding precommit hook as well as workflow --- .github/workflows/code-quality-check.yml | 28 +++++++++++++----------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/.github/workflows/code-quality-check.yml b/.github/workflows/code-quality-check.yml index 41a2d032..7cf41f42 100644 --- a/.github/workflows/code-quality-check.yml +++ b/.github/workflows/code-quality-check.yml @@ -64,7 +64,7 @@ jobs: echo "Found files to check:" echo "$FILES" - # Process each file individually to better handle errors + # Process each file individually with better error handling MAX_ERRORS=10 FAILED_FILES="" @@ -72,22 +72,24 @@ jobs: echo "Checking $FILE..." # Run cpplint on a single file and capture output - OUTPUT=$(python -m cpplint --filter=-readability/todo --linelength=100 "$FILE" 2>&1) + OUTPUT=$(python -m cpplint --filter=-readability/todo --linelength=100 "$FILE" 2>&1 || true) # Display the output for this file echo "$OUTPUT" - # Look for "Total errors found: X" in the output - if [[ "$OUTPUT" =~ "Total errors found: "([0-9]+) ]]; then - ERROR_COUNT="${BASH_REMATCH[1]}" - echo "File $FILE has $ERROR_COUNT errors" - - # Check if over threshold - if [ "$ERROR_COUNT" -gt "$MAX_ERRORS" ]; then - FAILED_FILES="$FAILED_FILES\n- $FILE ($ERROR_COUNT errors)" - fi - else - echo "Warning: Could not determine error count for $FILE" + # Extract error count more reliably + ERROR_COUNT=$(echo "$OUTPUT" | grep -o 'Total errors found: [0-9]*' | grep -o '[0-9]*' || echo "0") + + # If we couldn't extract a count, default to 0 + if [ -z "$ERROR_COUNT" ]; then + ERROR_COUNT=0 + fi + + echo "File $FILE has $ERROR_COUNT errors" + + # Check if over threshold + if [ "$ERROR_COUNT" -gt "$MAX_ERRORS" ]; then + FAILED_FILES="$FAILED_FILES\n- $FILE ($ERROR_COUNT errors)" fi done From 9fca5aa026c128bb6a67ec3411a26fe88c065f96 Mon Sep 17 00:00:00 2001 From: Jahnvi Thakkar Date: Wed, 10 Sep 2025 12:54:45 +0530 Subject: [PATCH 19/20] Adding pre-commit in requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index a4312a3d..018ec258 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ pybind11 coverage unittest-xml-reporting setuptools +pre-commit \ No newline at end of file From 850ce797e1adc35cdec14020a7af26888d0df2e9 Mon Sep 17 00:00:00 2001 From: Jahnvi Thakkar Date: Fri, 12 Sep 2025 09:23:51 +0530 Subject: [PATCH 20/20] Adding pre-commit in requirements.txt --- requirements.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 018ec258..1db51b1e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,6 @@ pybind11 coverage unittest-xml-reporting setuptools -pre-commit \ No newline at end of file +pre-commit +cpplint +pylint \ No newline at end of file