|  | 
| 25 | 25 | 
 | 
| 26 | 26 | # Add SQL_WMETADATA constant for metadata decoding configuration | 
| 27 | 27 | 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 | 
| 28 | 30 | 
 | 
| 29 | 31 | # UTF-16 encoding variants that should use SQL_WCHAR by default | 
| 30 | 32 | UTF16_ENCODINGS = frozenset([ | 
| @@ -872,7 +874,214 @@ def getinfo(self, info_type): | 
| 872 | 874 |  ddbc_error="Cannot get info on closed connection", | 
| 873 | 875 |  ) | 
| 874 | 876 | 
 | 
| 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 | 
| 876 | 1085 | 
 | 
| 877 | 1086 |  def commit(self) -> None: | 
| 878 | 1087 |  """ | 
|  | 
0 commit comments