Skip to content

Commit ec73b76

Browse files
authored
FIX: Correcting getinfo API (#249)
### Work Item / Issue Reference <!-- IMPORTANT: Please follow the PR template guidelines below. For mssql-python maintainers: Insert your ADO Work Item ID below (e.g. AB#37452) For external contributors: Insert Github Issue number below (e.g. #149) Only one reference is required - either GitHub issue OR ADO Work Item. --> <!-- mssql-python maintainers: ADO Work Item --> > [AB#34911](https://sqlclientdrivers.visualstudio.com/c6d89619-62de-46a0-8b46-70b92a84d85e/_workitems/edit/34911) ------------------------------------------------------------------- ### Summary This pull request refactors and improves the handling of ODBC `SQLGetInfo` metadata retrieval in the `mssql_python` driver. The main changes include a more robust and consistent method for returning info values from the C++ layer, enhanced decoding and type handling in Python, and better exposure of constants for users. These updates should make metadata access more reliable and easier to use, especially across different drivers and platforms. **ODBC Info Retrieval Refactor** * The C++ `getInfo` method in `connection.cpp` now always returns a dictionary containing the raw bytes, length, and info type, instead of attempting to interpret the result as a string or integer. This enables more consistent handling in Python and avoids issues with ambiguous types. * The Python `getinfo` method in `connection.py` has been rewritten to robustly interpret the raw result dictionary, properly decode string and Y/N types, and handle various numeric formats. It includes improved error handling and logging for easier debugging. **Constants and Metadata Exposure** * `GetInfoConstants` is now imported and its members (such as `SQL_DRIVER_NAME`, `SQL_SERVER_NAME`, etc.) are exported at the module level in `__init__.py`. This makes it easier for users to reference standard info types. * A new `get_info_constants()` function is provided to return all available `GetInfoConstants` as a dictionary, simplifying programmatic access. * Added missing constant `SQL_PROCEDURE_TERM` to the `GetInfoConstants` enum. **Testing Improvements** * Added debug print statements in `test_getinfo_standard_types` to help diagnose info type values during test runs. These changes collectively improve the reliability, usability, and maintainability of metadata access in the driver. --------- Co-authored-by: Jahnvi Thakkar <jathakkar@microsoft.com>
1 parent 8145182 commit ec73b76

File tree

6 files changed

+464
-179
lines changed

6 files changed

+464
-179
lines changed

mssql_python/__init__.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ def getDecimalSeparator():
140140
from .logging_config import setup_logging, get_logger
141141

142142
# Constants
143-
from .constants import ConstantsDDBC
143+
from .constants import ConstantsDDBC, GetInfoConstants
144144

145145
# Export specific constants for setencoding()
146146
SQL_CHAR = ConstantsDDBC.SQL_CHAR.value
@@ -205,3 +205,55 @@ def _custom_setattr(name, value):
205205
SQL_DATE = ConstantsDDBC.SQL_DATE.value
206206
SQL_TIME = ConstantsDDBC.SQL_TIME.value
207207
SQL_TIMESTAMP = ConstantsDDBC.SQL_TIMESTAMP.value
208+
209+
# Export GetInfo constants at module level
210+
# Driver and database information
211+
SQL_DRIVER_NAME = GetInfoConstants.SQL_DRIVER_NAME.value
212+
SQL_DRIVER_VER = GetInfoConstants.SQL_DRIVER_VER.value
213+
SQL_DRIVER_ODBC_VER = GetInfoConstants.SQL_DRIVER_ODBC_VER.value
214+
SQL_DATA_SOURCE_NAME = GetInfoConstants.SQL_DATA_SOURCE_NAME.value
215+
SQL_DATABASE_NAME = GetInfoConstants.SQL_DATABASE_NAME.value
216+
SQL_SERVER_NAME = GetInfoConstants.SQL_SERVER_NAME.value
217+
SQL_USER_NAME = GetInfoConstants.SQL_USER_NAME.value
218+
219+
# SQL conformance and support
220+
SQL_SQL_CONFORMANCE = GetInfoConstants.SQL_SQL_CONFORMANCE.value
221+
SQL_KEYWORDS = GetInfoConstants.SQL_KEYWORDS.value
222+
SQL_IDENTIFIER_QUOTE_CHAR = GetInfoConstants.SQL_IDENTIFIER_QUOTE_CHAR.value
223+
SQL_SEARCH_PATTERN_ESCAPE = GetInfoConstants.SQL_SEARCH_PATTERN_ESCAPE.value
224+
225+
# Catalog and schema support
226+
SQL_CATALOG_TERM = GetInfoConstants.SQL_CATALOG_TERM.value
227+
SQL_SCHEMA_TERM = GetInfoConstants.SQL_SCHEMA_TERM.value
228+
SQL_TABLE_TERM = GetInfoConstants.SQL_TABLE_TERM.value
229+
SQL_PROCEDURE_TERM = GetInfoConstants.SQL_PROCEDURE_TERM.value
230+
231+
# Transaction support
232+
SQL_TXN_CAPABLE = GetInfoConstants.SQL_TXN_CAPABLE.value
233+
SQL_DEFAULT_TXN_ISOLATION = GetInfoConstants.SQL_DEFAULT_TXN_ISOLATION.value
234+
235+
# Data type support
236+
SQL_NUMERIC_FUNCTIONS = GetInfoConstants.SQL_NUMERIC_FUNCTIONS.value
237+
SQL_STRING_FUNCTIONS = GetInfoConstants.SQL_STRING_FUNCTIONS.value
238+
SQL_DATETIME_FUNCTIONS = GetInfoConstants.SQL_DATETIME_FUNCTIONS.value
239+
240+
# Limits
241+
SQL_MAX_COLUMN_NAME_LEN = GetInfoConstants.SQL_MAX_COLUMN_NAME_LEN.value
242+
SQL_MAX_TABLE_NAME_LEN = GetInfoConstants.SQL_MAX_TABLE_NAME_LEN.value
243+
SQL_MAX_SCHEMA_NAME_LEN = GetInfoConstants.SQL_MAX_SCHEMA_NAME_LEN.value
244+
SQL_MAX_CATALOG_NAME_LEN = GetInfoConstants.SQL_MAX_CATALOG_NAME_LEN.value
245+
SQL_MAX_IDENTIFIER_LEN = GetInfoConstants.SQL_MAX_IDENTIFIER_LEN.value
246+
247+
# Also provide a function to get all constants
248+
def get_info_constants():
249+
"""
250+
Returns a dictionary of all available GetInfo constants.
251+
252+
This provides all SQLGetInfo constants that can be used with the Connection.getinfo() method
253+
to retrieve metadata about the database server and driver.
254+
255+
Returns:
256+
dict: Dictionary mapping constant names to their integer values
257+
"""
258+
return {name: member.value for name, member in GetInfoConstants.__members__.items()}
259+

mssql_python/connection.py

Lines changed: 210 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525

2626
# Add SQL_WMETADATA constant for metadata decoding configuration
2727
SQL_WMETADATA = -99 # Special flag for column name decoding
28+
# Threshold to determine if an info type is string-based
29+
INFO_TYPE_STRING_THRESHOLD = 10000
2830

2931
# UTF-16 encoding variants that should use SQL_WCHAR by default
3032
UTF16_ENCODINGS = frozenset([
@@ -872,7 +874,214 @@ def getinfo(self, info_type):
872874
ddbc_error="Cannot get info on closed connection",
873875
)
874876

875-
return self._conn.get_info(info_type)
877+
# Check that info_type is an integer
878+
if not isinstance(info_type, int):
879+
raise ValueError(f"info_type must be an integer, got {type(info_type).__name__}")
880+
881+
# Check for invalid info_type values
882+
if info_type < 0:
883+
log('warning', f"Invalid info_type: {info_type}. Must be a positive integer.")
884+
return None
885+
886+
# Get the raw result from the C++ layer
887+
try:
888+
raw_result = self._conn.get_info(info_type)
889+
except Exception as e:
890+
# Log the error and return None for invalid info types
891+
log('warning', f"getinfo({info_type}) failed: {e}")
892+
return None
893+
894+
if raw_result is None:
895+
return None
896+
897+
# Check if the result is already a simple type
898+
if isinstance(raw_result, (str, int, bool)):
899+
return raw_result
900+
901+
# If it's a dictionary with data and metadata
902+
if isinstance(raw_result, dict) and "data" in raw_result:
903+
# Extract data and metadata from the raw result
904+
data = raw_result["data"]
905+
length = raw_result["length"]
906+
907+
# Debug logging to understand the issue better
908+
log('debug', f"getinfo: info_type={info_type}, length={length}, data_type={type(data)}")
909+
910+
# Define constants for different return types
911+
# String types - these return strings in pyodbc
912+
string_type_constants = {
913+
GetInfoConstants.SQL_DATA_SOURCE_NAME.value,
914+
GetInfoConstants.SQL_DRIVER_NAME.value,
915+
GetInfoConstants.SQL_DRIVER_VER.value,
916+
GetInfoConstants.SQL_SERVER_NAME.value,
917+
GetInfoConstants.SQL_USER_NAME.value,
918+
GetInfoConstants.SQL_DRIVER_ODBC_VER.value,
919+
GetInfoConstants.SQL_IDENTIFIER_QUOTE_CHAR.value,
920+
GetInfoConstants.SQL_CATALOG_NAME_SEPARATOR.value,
921+
GetInfoConstants.SQL_CATALOG_TERM.value,
922+
GetInfoConstants.SQL_SCHEMA_TERM.value,
923+
GetInfoConstants.SQL_TABLE_TERM.value,
924+
GetInfoConstants.SQL_KEYWORDS.value,
925+
GetInfoConstants.SQL_PROCEDURE_TERM.value,
926+
GetInfoConstants.SQL_SPECIAL_CHARACTERS.value,
927+
GetInfoConstants.SQL_SEARCH_PATTERN_ESCAPE.value
928+
}
929+
930+
# Boolean 'Y'/'N' types
931+
yn_type_constants = {
932+
GetInfoConstants.SQL_ACCESSIBLE_PROCEDURES.value,
933+
GetInfoConstants.SQL_ACCESSIBLE_TABLES.value,
934+
GetInfoConstants.SQL_DATA_SOURCE_READ_ONLY.value,
935+
GetInfoConstants.SQL_EXPRESSIONS_IN_ORDERBY.value,
936+
GetInfoConstants.SQL_LIKE_ESCAPE_CLAUSE.value,
937+
GetInfoConstants.SQL_MULTIPLE_ACTIVE_TXN.value,
938+
GetInfoConstants.SQL_NEED_LONG_DATA_LEN.value,
939+
GetInfoConstants.SQL_PROCEDURES.value
940+
}
941+
942+
# Numeric type constants that return integers
943+
numeric_type_constants = {
944+
GetInfoConstants.SQL_MAX_COLUMN_NAME_LEN.value,
945+
GetInfoConstants.SQL_MAX_TABLE_NAME_LEN.value,
946+
GetInfoConstants.SQL_MAX_SCHEMA_NAME_LEN.value,
947+
GetInfoConstants.SQL_MAX_CATALOG_NAME_LEN.value,
948+
GetInfoConstants.SQL_MAX_IDENTIFIER_LEN.value,
949+
GetInfoConstants.SQL_MAX_STATEMENT_LEN.value,
950+
GetInfoConstants.SQL_MAX_DRIVER_CONNECTIONS.value,
951+
GetInfoConstants.SQL_NUMERIC_FUNCTIONS.value,
952+
GetInfoConstants.SQL_STRING_FUNCTIONS.value,
953+
GetInfoConstants.SQL_DATETIME_FUNCTIONS.value,
954+
GetInfoConstants.SQL_TXN_CAPABLE.value,
955+
GetInfoConstants.SQL_DEFAULT_TXN_ISOLATION.value,
956+
GetInfoConstants.SQL_CURSOR_COMMIT_BEHAVIOR.value
957+
}
958+
959+
# Determine the type of information we're dealing with
960+
is_string_type = info_type > INFO_TYPE_STRING_THRESHOLD or info_type in string_type_constants
961+
is_yn_type = info_type in yn_type_constants
962+
is_numeric_type = info_type in numeric_type_constants
963+
964+
# Process the data based on type
965+
if is_string_type:
966+
# For string data, ensure we properly handle the byte array
967+
if isinstance(data, bytes):
968+
# Make sure we use the correct amount of data based on length
969+
actual_data = data[:length]
970+
971+
# Now decode the string data
972+
try:
973+
return actual_data.decode('utf-8').rstrip('\0')
974+
except UnicodeDecodeError:
975+
try:
976+
return actual_data.decode('latin1').rstrip('\0')
977+
except Exception as e:
978+
log('error', f"Failed to decode string in getinfo: {e}. Returning None to avoid silent corruption.")
979+
# Explicitly return None to signal decoding failure
980+
return None
981+
else:
982+
# If it's not bytes, return as is
983+
return data
984+
elif is_yn_type:
985+
# For Y/N types, pyodbc returns a string 'Y' or 'N'
986+
if isinstance(data, bytes) and length >= 1:
987+
byte_val = data[0]
988+
if byte_val in (b'Y'[0], b'y'[0], 1):
989+
return 'Y'
990+
else:
991+
return 'N'
992+
else:
993+
# If it's not a byte or we can't determine, default to 'N'
994+
return 'N'
995+
elif is_numeric_type:
996+
# Handle numeric types based on length
997+
if isinstance(data, bytes):
998+
# Map byte length → signed int size
999+
int_sizes = {
1000+
1: lambda d: int(d[0]),
1001+
2: lambda d: int.from_bytes(d[:2], "little", signed=True),
1002+
4: lambda d: int.from_bytes(d[:4], "little", signed=True),
1003+
8: lambda d: int.from_bytes(d[:8], "little", signed=True),
1004+
}
1005+
1006+
# Direct numeric conversion if supported length
1007+
if length in int_sizes:
1008+
result = int_sizes[length](data)
1009+
return int(result)
1010+
1011+
# Helper: check if all chars are digits
1012+
def is_digit_bytes(b: bytes) -> bool:
1013+
return all(c in b"0123456789" for c in b)
1014+
1015+
# Helper: check if bytes are ASCII-printable or NUL padded
1016+
def is_printable_bytes(b: bytes) -> bool:
1017+
return all(32 <= c <= 126 or c == 0 for c in b)
1018+
1019+
chunk = data[:length]
1020+
1021+
# Try interpret as integer string
1022+
if is_digit_bytes(chunk):
1023+
return int(chunk)
1024+
1025+
# Try decode as ASCII/UTF-8 string
1026+
if is_printable_bytes(chunk):
1027+
str_val = chunk.decode("utf-8", errors="replace").rstrip("\0")
1028+
return int(str_val) if str_val.isdigit() else str_val
1029+
1030+
# For 16-bit values that might be returned for max lengths
1031+
if length == 2:
1032+
return int.from_bytes(data[:2], "little", signed=True)
1033+
1034+
# For 32-bit values (common for bitwise flags)
1035+
if length == 4:
1036+
return int.from_bytes(data[:4], "little", signed=True)
1037+
1038+
# Fallback: try to convert to int if possible
1039+
try:
1040+
if length <= 8:
1041+
return int.from_bytes(data[:length], "little", signed=True)
1042+
except Exception:
1043+
pass
1044+
1045+
# Last resort: return as integer if all else fails
1046+
try:
1047+
return int.from_bytes(data[:min(length, 8)], "little", signed=True)
1048+
except Exception:
1049+
return 0
1050+
elif isinstance(data, (int, float)):
1051+
# Already numeric
1052+
return int(data)
1053+
else:
1054+
# Try to convert to int if it's a string
1055+
try:
1056+
if isinstance(data, str) and data.isdigit():
1057+
return int(data)
1058+
except Exception:
1059+
pass
1060+
1061+
# Return as is if we can't convert
1062+
return data
1063+
else:
1064+
# For other types, try to determine the most appropriate type
1065+
if isinstance(data, bytes):
1066+
# Try to convert to string first
1067+
try:
1068+
return data[:length].decode('utf-8').rstrip('\0')
1069+
except UnicodeDecodeError:
1070+
pass
1071+
1072+
# Try to convert to int for short binary data
1073+
try:
1074+
if length <= 8:
1075+
return int.from_bytes(data[:length], "little", signed=True)
1076+
except Exception:
1077+
pass
1078+
1079+
# Return as is if we can't determine
1080+
return data
1081+
else:
1082+
return data
1083+
1084+
return raw_result # Return as-is
8761085

8771086
def commit(self) -> None:
8781087
"""

mssql_python/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ class GetInfoConstants(Enum):
247247
SQL_BATCH_ROW_COUNT = 120
248248
SQL_PARAM_ARRAY_ROW_COUNTS = 153
249249
SQL_PARAM_ARRAY_SELECTS = 154
250+
SQL_PROCEDURE_TERM = 40
250251

251252
# Positioned statement support
252253
SQL_POSITIONED_STATEMENTS = 80

mssql_python/msvcp140.dll

562 KB
Binary file not shown.

0 commit comments

Comments
 (0)