Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 8 additions & 167 deletions mssql_python/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import weakref
from typing import Dict

# Import settings from helpers to avoid circular imports
# Import settings from helpers module
from .helpers import Settings, get_settings, _settings, _settings_lock

# Driver version
Expand Down Expand Up @@ -65,7 +65,7 @@
from .logging import logger, setup_logging, driver_logger

# Constants
from .constants import ConstantsDDBC, GetInfoConstants
from .constants import ConstantsDDBC, GetInfoConstants, get_info_constants

# Pooling
from .pooling import PoolingManager
Expand Down Expand Up @@ -116,100 +116,16 @@ def _cleanup_connections():
# GLOBALS
# Read-Only
apilevel: str = "2.0"
paramstyle: str = "qmark"
paramstyle: str = "pyformat"
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The paramstyle is changed from "qmark" to "pyformat", but this is a breaking API change. According to PEP 249, paramstyle indicates which parameter marker format is supported. Changing this from "qmark" (?) to "pyformat" (%(name)s) could break existing code that relies on this value to determine how to format parameters. While the implementation now supports both styles automatically, the paramstyle should reflect the primary/preferred style or be documented as supporting multiple styles. Consider if this should be "pyformat" or remain "qmark" with documentation noting that both are supported.

Suggested change
paramstyle: str = "pyformat"
# PEP 249 paramstyle: keep "qmark" for backward compatibility; implementation also supports "pyformat".
paramstyle: str = "qmark"

Copilot uses AI. Check for mistakes.
threadsafety: int = 1

# Set the initial decimal separator in C++
try:
from .ddbc_bindings import DDBCSetDecimalSeparator
# Create decimal separator control functions bound to our settings
from .decimal_config import create_decimal_separator_functions

DDBCSetDecimalSeparator(_settings.decimal_separator)
except ImportError:
# Handle case where ddbc_bindings is not available
DDBCSetDecimalSeparator = None
setDecimalSeparator, getDecimalSeparator = create_decimal_separator_functions(_settings)


# New functions for decimal separator control
def setDecimalSeparator(separator: str) -> None:
"""
Sets the decimal separator character used when parsing NUMERIC/DECIMAL values
from the database, e.g. the "." in "1,234.56".

The default is to use the current locale's "decimal_point" value when the module
was first imported, or "." if the locale is not available. This function overrides
the default.

Args:
separator (str): The character to use as decimal separator

Raises:
ValueError: If the separator is not a single character string
"""
# Type validation
if not isinstance(separator, str):
raise ValueError("Decimal separator must be a string")

# Length validation
if len(separator) == 0:
raise ValueError("Decimal separator cannot be empty")

if len(separator) > 1:
raise ValueError("Decimal separator must be a single character")

# Character validation
if separator.isspace():
raise ValueError("Whitespace characters are not allowed as decimal separators")

# Check for specific disallowed characters
if separator in ["\t", "\n", "\r", "\v", "\f"]:
raise ValueError(
f"Control character '{repr(separator)}' is not allowed as a decimal separator"
)

# Set in Python side settings
_settings.decimal_separator = separator

# Update the C++ side
if DDBCSetDecimalSeparator is not None:
DDBCSetDecimalSeparator(separator)


def getDecimalSeparator() -> str:
"""
Returns the decimal separator character used when parsing NUMERIC/DECIMAL values
from the database.

Returns:
str: The current decimal separator character
"""
return _settings.decimal_separator


# Export specific constants for setencoding()
SQL_CHAR: int = ConstantsDDBC.SQL_CHAR.value
SQL_WCHAR: int = ConstantsDDBC.SQL_WCHAR.value
SQL_WMETADATA: int = -99

# Export connection attribute constants for set_attr()
# Only include driver-level attributes that the SQL Server ODBC driver can handle directly

# Core driver-level attributes
SQL_ATTR_ACCESS_MODE: int = ConstantsDDBC.SQL_ATTR_ACCESS_MODE.value
SQL_ATTR_CONNECTION_TIMEOUT: int = ConstantsDDBC.SQL_ATTR_CONNECTION_TIMEOUT.value
SQL_ATTR_CURRENT_CATALOG: int = ConstantsDDBC.SQL_ATTR_CURRENT_CATALOG.value
SQL_ATTR_LOGIN_TIMEOUT: int = ConstantsDDBC.SQL_ATTR_LOGIN_TIMEOUT.value
SQL_ATTR_PACKET_SIZE: int = ConstantsDDBC.SQL_ATTR_PACKET_SIZE.value
SQL_ATTR_TXN_ISOLATION: int = ConstantsDDBC.SQL_ATTR_TXN_ISOLATION.value

# Transaction Isolation Level Constants
SQL_TXN_READ_UNCOMMITTED: int = ConstantsDDBC.SQL_TXN_READ_UNCOMMITTED.value
SQL_TXN_READ_COMMITTED: int = ConstantsDDBC.SQL_TXN_READ_COMMITTED.value
SQL_TXN_REPEATABLE_READ: int = ConstantsDDBC.SQL_TXN_REPEATABLE_READ.value
SQL_TXN_SERIALIZABLE: int = ConstantsDDBC.SQL_TXN_SERIALIZABLE.value

# Access Mode Constants
SQL_MODE_READ_WRITE: int = ConstantsDDBC.SQL_MODE_READ_WRITE.value
SQL_MODE_READ_ONLY: int = ConstantsDDBC.SQL_MODE_READ_ONLY.value
# Import all module-level constants from constants module
from .constants import * # noqa: F401, F403


Comment on lines +127 to 130
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The wildcard import from .constants import * makes it difficult to track which names are being imported into the module namespace. While all is defined in constants.py to control exports, this can make the code harder to maintain and debug. Additionally, the # noqa: F401, F403 suppresses linting warnings about unused imports and undefined names from wildcard imports. Consider explicitly importing the needed constants or at least document what's being imported.

Suggested change
# Import all module-level constants from constants module
from .constants import * # noqa: F401, F403
# Import constants module and re-export its public names defined in __all__
from . import constants as _constants
for _name in getattr(_constants, "__all__", ()):
setattr(sys.modules[__name__], _name, getattr(_constants, _name))

Copilot uses AI. Check for mistakes.
def pooling(max_size: int = 100, idle_timeout: int = 600, enabled: bool = True) -> None:
Expand Down Expand Up @@ -249,81 +165,6 @@ def _custom_setattr(name, value):
sys.modules[__name__].__setattr__ = _custom_setattr


# Export SQL constants at module level
SQL_VARCHAR: int = ConstantsDDBC.SQL_VARCHAR.value
SQL_LONGVARCHAR: int = ConstantsDDBC.SQL_LONGVARCHAR.value
SQL_WVARCHAR: int = ConstantsDDBC.SQL_WVARCHAR.value
SQL_WLONGVARCHAR: int = ConstantsDDBC.SQL_WLONGVARCHAR.value
SQL_DECIMAL: int = ConstantsDDBC.SQL_DECIMAL.value
SQL_NUMERIC: int = ConstantsDDBC.SQL_NUMERIC.value
SQL_BIT: int = ConstantsDDBC.SQL_BIT.value
SQL_TINYINT: int = ConstantsDDBC.SQL_TINYINT.value
SQL_SMALLINT: int = ConstantsDDBC.SQL_SMALLINT.value
SQL_INTEGER: int = ConstantsDDBC.SQL_INTEGER.value
SQL_BIGINT: int = ConstantsDDBC.SQL_BIGINT.value
SQL_REAL: int = ConstantsDDBC.SQL_REAL.value
SQL_FLOAT: int = ConstantsDDBC.SQL_FLOAT.value
SQL_DOUBLE: int = ConstantsDDBC.SQL_DOUBLE.value
SQL_BINARY: int = ConstantsDDBC.SQL_BINARY.value
SQL_VARBINARY: int = ConstantsDDBC.SQL_VARBINARY.value
SQL_LONGVARBINARY: int = ConstantsDDBC.SQL_LONGVARBINARY.value
SQL_DATE: int = ConstantsDDBC.SQL_DATE.value
SQL_TIME: int = ConstantsDDBC.SQL_TIME.value
SQL_TIMESTAMP: int = ConstantsDDBC.SQL_TIMESTAMP.value

# Export GetInfo constants at module level
# Driver and database information
SQL_DRIVER_NAME: int = GetInfoConstants.SQL_DRIVER_NAME.value
SQL_DRIVER_VER: int = GetInfoConstants.SQL_DRIVER_VER.value
SQL_DRIVER_ODBC_VER: int = GetInfoConstants.SQL_DRIVER_ODBC_VER.value
SQL_DATA_SOURCE_NAME: int = GetInfoConstants.SQL_DATA_SOURCE_NAME.value
SQL_DATABASE_NAME: int = GetInfoConstants.SQL_DATABASE_NAME.value
SQL_SERVER_NAME: int = GetInfoConstants.SQL_SERVER_NAME.value
SQL_USER_NAME: int = GetInfoConstants.SQL_USER_NAME.value

# SQL conformance and support
SQL_SQL_CONFORMANCE: int = GetInfoConstants.SQL_SQL_CONFORMANCE.value
SQL_KEYWORDS: int = GetInfoConstants.SQL_KEYWORDS.value
SQL_IDENTIFIER_QUOTE_CHAR: int = GetInfoConstants.SQL_IDENTIFIER_QUOTE_CHAR.value
SQL_SEARCH_PATTERN_ESCAPE: int = GetInfoConstants.SQL_SEARCH_PATTERN_ESCAPE.value

# Catalog and schema support
SQL_CATALOG_TERM: int = GetInfoConstants.SQL_CATALOG_TERM.value
SQL_SCHEMA_TERM: int = GetInfoConstants.SQL_SCHEMA_TERM.value
SQL_TABLE_TERM: int = GetInfoConstants.SQL_TABLE_TERM.value
SQL_PROCEDURE_TERM: int = GetInfoConstants.SQL_PROCEDURE_TERM.value

# Transaction support
SQL_TXN_CAPABLE: int = GetInfoConstants.SQL_TXN_CAPABLE.value
SQL_DEFAULT_TXN_ISOLATION: int = GetInfoConstants.SQL_DEFAULT_TXN_ISOLATION.value

# Data type support
SQL_NUMERIC_FUNCTIONS: int = GetInfoConstants.SQL_NUMERIC_FUNCTIONS.value
SQL_STRING_FUNCTIONS: int = GetInfoConstants.SQL_STRING_FUNCTIONS.value
SQL_DATETIME_FUNCTIONS: int = GetInfoConstants.SQL_DATETIME_FUNCTIONS.value

# Limits
SQL_MAX_COLUMN_NAME_LEN: int = GetInfoConstants.SQL_MAX_COLUMN_NAME_LEN.value
SQL_MAX_TABLE_NAME_LEN: int = GetInfoConstants.SQL_MAX_TABLE_NAME_LEN.value
SQL_MAX_SCHEMA_NAME_LEN: int = GetInfoConstants.SQL_MAX_SCHEMA_NAME_LEN.value
SQL_MAX_CATALOG_NAME_LEN: int = GetInfoConstants.SQL_MAX_CATALOG_NAME_LEN.value
SQL_MAX_IDENTIFIER_LEN: int = GetInfoConstants.SQL_MAX_IDENTIFIER_LEN.value


# Also provide a function to get all constants
def get_info_constants() -> Dict[str, int]:
"""
Returns a dictionary of all available GetInfo constants.

This provides all SQLGetInfo constants that can be used with the Connection.getinfo() method
to retrieve metadata about the database server and driver.

Returns:
dict: Dictionary mapping constant names to their integer values
"""
return {name: member.value for name, member in GetInfoConstants.__members__.items()}


# Create a custom module class that uses properties instead of __setattr__
class _MSSQLModule(types.ModuleType):
@property
Expand Down
108 changes: 108 additions & 0 deletions mssql_python/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -502,3 +502,111 @@ def get_attribute_set_timing(attribute):
# internally.
"packetsize": "PacketSize",
}


def get_info_constants() -> Dict[str, int]:
"""
Returns a dictionary of all available GetInfo constants.

This provides all SQLGetInfo constants that can be used with the Connection.getinfo() method
to retrieve metadata about the database server and driver.

Returns:
dict: Dictionary mapping constant names to their integer values
"""
return {name: member.value for name, member in GetInfoConstants.__members__.items()}


# Automatically export selected enum constants as module-level attributes
# This allows: from mssql_python import SQL_VARCHAR
# Instead of: from mssql_python.constants import ConstantsDDBC; ConstantsDDBC.SQL_VARCHAR.value

# Define which ConstantsDDBC members should be exported as public API
# Internal/driver-manager-dependent constants are excluded
_DDBC_PUBLIC_API = {
# SQL Type constants
"SQL_CHAR",
"SQL_VARCHAR",
"SQL_LONGVARCHAR",
"SQL_WCHAR",
"SQL_WVARCHAR",
"SQL_WLONGVARCHAR",
"SQL_DECIMAL",
"SQL_NUMERIC",
"SQL_BIT",
"SQL_TINYINT",
"SQL_SMALLINT",
"SQL_INTEGER",
"SQL_BIGINT",
"SQL_REAL",
"SQL_FLOAT",
"SQL_DOUBLE",
"SQL_BINARY",
"SQL_VARBINARY",
"SQL_LONGVARBINARY",
"SQL_DATE",
"SQL_TIME",
"SQL_TIMESTAMP",
"SQL_TYPE_DATE",
"SQL_TYPE_TIME",
"SQL_TYPE_TIMESTAMP",
"SQL_GUID",
"SQL_XML",
# Connection attribute constants (ODBC-standard, driver-independent only)
"SQL_ATTR_ACCESS_MODE",
"SQL_ATTR_CONNECTION_TIMEOUT",
"SQL_ATTR_CURRENT_CATALOG",
"SQL_ATTR_LOGIN_TIMEOUT",
"SQL_ATTR_PACKET_SIZE",
"SQL_ATTR_TXN_ISOLATION",
# Transaction isolation levels
"SQL_TXN_READ_UNCOMMITTED",
"SQL_TXN_READ_COMMITTED",
"SQL_TXN_REPEATABLE_READ",
"SQL_TXN_SERIALIZABLE",
# Access modes
"SQL_MODE_READ_WRITE",
"SQL_MODE_READ_ONLY",
}

# Get current module's globals for dynamic export
_module_globals = globals()
_exported_names = []

# Export selected ConstantsDDBC enum members as module-level constants
for _name, _member in ConstantsDDBC.__members__.items():
if _name in _DDBC_PUBLIC_API:
_module_globals[_name] = _member.value
_exported_names.append(_name)

# Export all GetInfoConstants enum members as module-level constants
for _name, _member in GetInfoConstants.__members__.items():
_module_globals[_name] = _member.value
_exported_names.append(_name)

# Export all AuthType enum members as module-level constants
for _name, _member in AuthType.__members__.items():
_module_globals[_name] = _member.value
_exported_names.append(_name)

# SQLTypes is not an Enum, it's a regular class - don't iterate it

# Add special constant not in enum
SQL_WMETADATA: int = -99
_exported_names.append("SQL_WMETADATA")

# Define __all__ for controlled exports
__all__ = [
# Enum classes themselves
"ConstantsDDBC",
"GetInfoConstants",
"AuthType",
"SQLTypes",
# Helper function
"get_info_constants",
# All dynamically exported constants
*_exported_names,
]

# Clean up temporary variables
del _module_globals, _exported_names, _name, _member, _DDBC_PUBLIC_API
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cleanup at line 612 deletes temporary variables including _name and _member, but these are loop variables from the for loops. In Python 3, loop variables persist after the loop completes, so this deletion is valid. However, _DDBC_PUBLIC_API is deleted but it's a set that was defined at the module level and could be useful for introspection or testing. Consider keeping it or making it part of all if it's meant to be internal.

Suggested change
del _module_globals, _exported_names, _name, _member, _DDBC_PUBLIC_API
del _module_globals, _exported_names, _name, _member

Copilot uses AI. Check for mistakes.
Loading
Loading