-
Notifications
You must be signed in to change notification settings - Fork 36
FIX: Cursor.describe invalid data #355
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c403899
f96d14c
e135e40
38966d0
3f52dd7
38b041b
139994a
6fd823e
a7d9b8e
646ef04
8018872
67268e5
d335ed0
2dc93f3
eee5256
d289a9b
ced5fc2
0568451
257f0f6
033f7cd
63c08f5
6adeead
7b6da4e
42d3e09
31a6d13
6eb43ea
327ee42
7a487b8
839019c
466a4d5
5e67a1b
f484e8d
c0c2117
b5460dd
bced470
3d09f1a
fe2a042
9913467
c0b619e
96b3240
aa6b071
f88b75e
0011cf2
f41a0cb
fb30fc6
4761dda
c5a4ae3
0b3b36c
117b3b4
fb9c3e6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -48,6 +48,110 @@ | |
| MONEY_MAX: decimal.Decimal = decimal.Decimal("922337203685477.5807") | ||
|
|
||
|
|
||
| class SQLTypeCode: | ||
| """ | ||
| A dual-compatible type code that compares equal to both SQL type integers and Python types. | ||
|
|
||
| This class maintains backwards compatibility with code that checks | ||
| `cursor.description[i][1] == str` while also supporting DB-API 2.0 | ||
| compliant code that checks `cursor.description[i][1] == -9`. | ||
|
|
||
| Examples: | ||
| >>> type_code = SQLTypeCode(-9, str) | ||
| >>> type_code == str # Backwards compatible with pandas, etc. | ||
| True | ||
| >>> type_code == -9 # DB-API 2.0 compliant | ||
| True | ||
| >>> int(type_code) # Get the raw SQL type code | ||
| -9 | ||
| """ | ||
|
|
||
| # SQL type code to Python type mapping (class-level cache) | ||
| _type_map = None | ||
|
|
||
| def __init__(self, type_code: int, python_type: type = None): | ||
| self.type_code = type_code | ||
| # If python_type not provided, look it up from the mapping | ||
| if python_type is None: | ||
| python_type = self._get_python_type(type_code) | ||
| self.python_type = python_type | ||
|
|
||
| @classmethod | ||
| def _get_type_map(cls): | ||
| """Lazily build the SQL to Python type mapping.""" | ||
| if cls._type_map is None: | ||
| cls._type_map = { | ||
| ddbc_sql_const.SQL_CHAR.value: str, | ||
| ddbc_sql_const.SQL_VARCHAR.value: str, | ||
| ddbc_sql_const.SQL_LONGVARCHAR.value: str, | ||
| ddbc_sql_const.SQL_WCHAR.value: str, | ||
| ddbc_sql_const.SQL_WVARCHAR.value: str, | ||
| ddbc_sql_const.SQL_WLONGVARCHAR.value: str, | ||
| ddbc_sql_const.SQL_INTEGER.value: int, | ||
| ddbc_sql_const.SQL_REAL.value: float, | ||
| ddbc_sql_const.SQL_FLOAT.value: float, | ||
| ddbc_sql_const.SQL_DOUBLE.value: float, | ||
| ddbc_sql_const.SQL_DECIMAL.value: decimal.Decimal, | ||
| ddbc_sql_const.SQL_NUMERIC.value: decimal.Decimal, | ||
| ddbc_sql_const.SQL_DATE.value: datetime.date, | ||
| ddbc_sql_const.SQL_TIMESTAMP.value: datetime.datetime, | ||
| ddbc_sql_const.SQL_TIME.value: datetime.time, | ||
dlevy-msft-sql marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ddbc_sql_const.SQL_SS_TIME2.value: datetime.time, # SQL Server TIME(n) | ||
| # ODBC 3.x date/time type codes | ||
| ddbc_sql_const.SQL_TYPE_DATE.value: datetime.date, | ||
| ddbc_sql_const.SQL_TYPE_TIME.value: datetime.time, | ||
| ddbc_sql_const.SQL_TYPE_TIMESTAMP.value: datetime.datetime, | ||
| ddbc_sql_const.SQL_TYPE_TIMESTAMP_WITH_TIMEZONE.value: datetime.datetime, | ||
| ddbc_sql_const.SQL_BIT.value: bool, | ||
| ddbc_sql_const.SQL_TINYINT.value: int, | ||
| ddbc_sql_const.SQL_SMALLINT.value: int, | ||
| ddbc_sql_const.SQL_BIGINT.value: int, | ||
| ddbc_sql_const.SQL_BINARY.value: bytes, | ||
| ddbc_sql_const.SQL_VARBINARY.value: bytes, | ||
| ddbc_sql_const.SQL_LONGVARBINARY.value: bytes, | ||
| ddbc_sql_const.SQL_GUID.value: uuid.UUID, | ||
| ddbc_sql_const.SQL_SS_UDT.value: bytes, | ||
| ddbc_sql_const.SQL_SS_XML.value: str, # SQL Server XML type (-152) | ||
| ddbc_sql_const.SQL_DATETIME2.value: datetime.datetime, | ||
| ddbc_sql_const.SQL_SMALLDATETIME.value: datetime.datetime, | ||
| ddbc_sql_const.SQL_DATETIMEOFFSET.value: datetime.datetime, | ||
| } | ||
| return cls._type_map | ||
|
|
||
| @classmethod | ||
| def _get_python_type(cls, sql_code: int) -> type: | ||
| """Get the Python type for a SQL type code.""" | ||
| return cls._get_type_map().get(sql_code, str) | ||
|
|
||
| def __eq__(self, other): | ||
| """Compare equal to both Python types and SQL integer codes.""" | ||
| if isinstance(other, type): | ||
| return self.python_type == other | ||
| if isinstance(other, int): | ||
| return self.type_code == other | ||
| if isinstance(other, SQLTypeCode): | ||
| return self.type_code == other.type_code | ||
| return False | ||
|
|
||
| def __ne__(self, other): | ||
| return not self.__eq__(other) | ||
|
|
||
| # Instances are intentionally unhashable because __eq__ allows | ||
| # comparisons to both Python types and integer SQL codes, and | ||
| # there is no single hash value that can be consistent with both. | ||
| __hash__ = None | ||
|
|
||
| def __int__(self): | ||
| return self.type_code | ||
dlevy-msft-sql marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| def __repr__(self): | ||
| type_name = self.python_type.__name__ if self.python_type else "Unknown" | ||
| return f"SQLTypeCode({self.type_code}, {type_name})" | ||
|
|
||
| def __str__(self): | ||
| return str(self.type_code) | ||
|
|
||
|
|
||
| class Cursor: # pylint: disable=too-many-instance-attributes,too-many-public-methods | ||
| """ | ||
| Represents a database cursor, which is used to manage the context of a fetch operation. | ||
|
|
@@ -142,6 +246,9 @@ def __init__(self, connection: "Connection", timeout: int = 0) -> None: | |
| ) | ||
| self.messages = [] # Store diagnostic messages | ||
|
|
||
| # Store raw column metadata for converter lookups | ||
| self._column_metadata = None | ||
|
|
||
| def _is_unicode_string(self, param: str) -> bool: | ||
| """ | ||
| Check if a string contains non-ASCII characters. | ||
|
|
@@ -756,6 +863,7 @@ def close(self) -> None: | |
| self.hstmt = None | ||
| logger.debug("SQLFreeHandle succeeded") | ||
| self._clear_rownumber() | ||
| self._column_metadata = None # Clear metadata to prevent memory leaks | ||
| self.closed = True | ||
|
|
||
| def _check_closed(self) -> None: | ||
|
|
@@ -942,8 +1050,12 @@ def _initialize_description(self, column_metadata: Optional[Any] = None) -> None | |
| """Initialize the description attribute from column metadata.""" | ||
| if not column_metadata: | ||
| self.description = None | ||
| self._column_metadata = None # Clear metadata too | ||
| return | ||
|
|
||
| # Store raw metadata for converter map building | ||
| self._column_metadata = column_metadata | ||
|
|
||
|
Comment on lines
1050
to
+1058
|
||
| description = [] | ||
| for _, col in enumerate(column_metadata): | ||
| # Get column name - lowercase it if the lowercase flag is set | ||
|
|
@@ -954,10 +1066,13 @@ def _initialize_description(self, column_metadata: Optional[Any] = None) -> None | |
| column_name = column_name.lower() | ||
|
|
||
| # Add to description tuple (7 elements as per PEP-249) | ||
| # Use SQLTypeCode for backwards-compatible type_code that works with both | ||
| # `desc[1] == str` (pandas) and `desc[1] == -9` (DB-API 2.0) | ||
| sql_type = col["DataType"] | ||
| description.append( | ||
| ( | ||
| column_name, # name | ||
| self._map_data_type(col["DataType"]), # type_code | ||
| SQLTypeCode(sql_type), # type_code - dual compatible | ||
| None, # display_size | ||
dlevy-msft-sql marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| col["ColumnSize"], # internal_size | ||
| col["ColumnSize"], # precision - should match ColumnSize | ||
|
|
@@ -975,18 +1090,17 @@ def _build_converter_map(self): | |
| """ | ||
| if ( | ||
| not self.description | ||
| or not self._column_metadata | ||
| or not hasattr(self.connection, "_output_converters") | ||
| or not self.connection._output_converters | ||
| ): | ||
| return None | ||
|
|
||
| converter_map = [] | ||
|
|
||
| for desc in self.description: | ||
| if desc is None: | ||
| converter_map.append(None) | ||
| continue | ||
| sql_type = desc[1] | ||
| for col_meta in self._column_metadata: | ||
| # Use the raw SQL type code from metadata, not the mapped Python type | ||
| sql_type = col_meta["DataType"] | ||
| converter = self.connection.get_output_converter(sql_type) | ||
| # If no converter found for the SQL type, try the WVARCHAR converter as a fallback | ||
| if converter is None: | ||
|
|
@@ -1022,41 +1136,6 @@ def _get_column_and_converter_maps(self): | |
|
|
||
| return column_map, converter_map | ||
|
|
||
| def _map_data_type(self, sql_type): | ||
| """ | ||
| Map SQL data type to Python data type. | ||
|
|
||
| Args: | ||
| sql_type: SQL data type. | ||
|
|
||
| Returns: | ||
| Corresponding Python data type. | ||
| """ | ||
| sql_to_python_type = { | ||
| ddbc_sql_const.SQL_INTEGER.value: int, | ||
| ddbc_sql_const.SQL_VARCHAR.value: str, | ||
| ddbc_sql_const.SQL_WVARCHAR.value: str, | ||
| ddbc_sql_const.SQL_CHAR.value: str, | ||
| ddbc_sql_const.SQL_WCHAR.value: str, | ||
| ddbc_sql_const.SQL_FLOAT.value: float, | ||
| ddbc_sql_const.SQL_DOUBLE.value: float, | ||
| ddbc_sql_const.SQL_DECIMAL.value: decimal.Decimal, | ||
| ddbc_sql_const.SQL_NUMERIC.value: decimal.Decimal, | ||
| ddbc_sql_const.SQL_DATE.value: datetime.date, | ||
| ddbc_sql_const.SQL_TIMESTAMP.value: datetime.datetime, | ||
| ddbc_sql_const.SQL_TIME.value: datetime.time, | ||
| ddbc_sql_const.SQL_BIT.value: bool, | ||
| ddbc_sql_const.SQL_TINYINT.value: int, | ||
| ddbc_sql_const.SQL_SMALLINT.value: int, | ||
| ddbc_sql_const.SQL_BIGINT.value: int, | ||
| ddbc_sql_const.SQL_BINARY.value: bytes, | ||
| ddbc_sql_const.SQL_VARBINARY.value: bytes, | ||
| ddbc_sql_const.SQL_LONGVARBINARY.value: bytes, | ||
| ddbc_sql_const.SQL_GUID.value: uuid.UUID, | ||
| # Add more mappings as needed | ||
| } | ||
| return sql_to_python_type.get(sql_type, str) | ||
|
|
||
| @property | ||
| def rownumber(self) -> int: | ||
| """ | ||
|
|
@@ -2756,7 +2835,13 @@ def __del__(self): | |
| Destructor to ensure the cursor is closed when it is no longer needed. | ||
| This is a safety net to ensure resources are cleaned up | ||
| even if close() was not called explicitly. | ||
dlevy-msft-sql marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| If the cursor is already closed, it will not raise an exception during cleanup. | ||
|
|
||
| Error handling: | ||
| This destructor performs best-effort cleanup only. Any exceptions raised | ||
| while closing the cursor are caught and, when possible, logged instead of | ||
| being propagated, because raising from __del__ can cause hard-to-debug | ||
| failures during garbage collection. During interpreter shutdown, logging | ||
| may be suppressed if the logging subsystem is no longer available. | ||
| """ | ||
| if "closed" not in self.__dict__ or not self.closed: | ||
| try: | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.