|
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