Skip to content

Commit 9d42d89

Browse files
authored
SNOW-2174093: improve error handling when closing cursor and connection in dbapi (#3487)
1 parent 583c46e commit 9d42d89

File tree

4 files changed

+143
-4
lines changed

4 files changed

+143
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#### Bug Fixes
2424

2525
- Fixed a bug caused by redundant validation when creating an iceberg table.
26+
- Fixed a bug in `DataFrameReader.dbapi` (PrPr) where closing the cursor or connection could unexpectedly raise an error and terminate the program.
2627

2728
### Snowpark Local Testing Updates
2829

src/snowflake/snowpark/_internal/data_source/datasource_reader.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,18 @@ def read(self, partition: str) -> Iterator[List[Any]]:
8383
else:
8484
raise ValueError("fetch size cannot be smaller than 0")
8585
finally:
86-
cursor.close()
87-
conn.close()
86+
try:
87+
cursor.close()
88+
except BaseException as exc:
89+
logger.debug(
90+
f"Failed to close cursor after reading data due to error: {exc!r}"
91+
)
92+
try:
93+
conn.close()
94+
except BaseException as exc:
95+
logger.debug(
96+
f"Failed to close connection after reading data due to error: {exc!r}"
97+
)
8898

8999
def data_source_data_to_pandas_df(self, data: List[Any]) -> "pd.DataFrame":
90100
# self.driver is guaranteed to be initialized in self.read() which is called prior to this method

src/snowflake/snowpark/_internal/data_source/drivers/base_driver.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,20 @@ def infer_schema_from_description_with_error_control(
8282
f" Please check the stack trace for more details."
8383
) from exc
8484
finally:
85-
cursor.close()
86-
conn.close()
85+
# Best effort to close cursor and connection; failures are non-critical and can be ignored.
86+
try:
87+
cursor.close()
88+
except BaseException as exc:
89+
logger.debug(
90+
f"Failed to close cursor after inferring schema from description due to error: {exc!r}"
91+
)
92+
93+
try:
94+
conn.close()
95+
except BaseException as exc:
96+
logger.debug(
97+
f"Failed to close connection after inferring schema from description due to error: {exc!r}"
98+
)
8799

88100
def udtf_ingestion(
89101
self,

tests/unit/test_data_source.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#
2+
# Copyright (c) 2012-2025 Snowflake Computing Inc. All rights reserved.
3+
#
4+
5+
import pytest
6+
import re
7+
from unittest.mock import Mock, patch
8+
from snowflake.snowpark._internal.data_source.drivers.base_driver import BaseDriver
9+
from snowflake.snowpark._internal.data_source.datasource_reader import DataSourceReader
10+
from snowflake.snowpark._internal.data_source.utils import DBMS_TYPE
11+
from snowflake.snowpark.types import StructType, StructField, StringType
12+
13+
14+
@pytest.mark.parametrize(
15+
"cursor_fails,conn_fails",
16+
[
17+
(True, False), # cursor.close() fails
18+
(False, True), # connection.close() fails
19+
(True, True), # both fail
20+
],
21+
)
22+
def test_close_error_handling(cursor_fails, conn_fails):
23+
"""Test that errors during cursor/connection close are handled gracefully."""
24+
# Setup mock driver
25+
mock_create_connection = Mock()
26+
driver = BaseDriver(mock_create_connection, DBMS_TYPE.UNKNOWN)
27+
28+
# Setup mocks
29+
mock_conn = Mock()
30+
mock_cursor = Mock()
31+
mock_conn.cursor.return_value = mock_cursor
32+
mock_create_connection.return_value = mock_conn
33+
34+
# Configure failures
35+
36+
if conn_fails:
37+
mock_conn.close.side_effect = Exception("Connection close failed")
38+
if cursor_fails:
39+
mock_cursor.close.side_effect = Exception("Cursor close failed")
40+
41+
# Mock schema inference to succeed
42+
expected_schema = StructType([StructField("test_col", StringType())])
43+
driver.infer_schema_from_description = Mock(return_value=expected_schema)
44+
45+
# Test - should succeed despite close errors and log the failure
46+
with patch(
47+
"snowflake.snowpark._internal.data_source.drivers.base_driver.logger"
48+
) as mock_logger:
49+
result = driver.infer_schema_from_description_with_error_control(
50+
"test_table", False
51+
)
52+
53+
assert result == expected_schema
54+
55+
# Use regex to match the log message flexibly
56+
mock_logger.debug.assert_called()
57+
args, kwargs = mock_logger.debug.call_args
58+
assert re.search(r"Failed to close", args[0])
59+
60+
61+
@pytest.mark.parametrize(
62+
"cursor_fails,conn_fails",
63+
[
64+
(True, False), # cursor.close() fails
65+
(False, True), # connection.close() fails
66+
(True, True), # both fail
67+
],
68+
)
69+
def test_datasource_reader_close_error_handling(cursor_fails, conn_fails):
70+
"""Test that DataSourceReader handles cursor/connection close errors gracefully."""
71+
# Setup mocks
72+
mock_create_connection = Mock()
73+
expected_schema = StructType([StructField("test_col", StringType())])
74+
75+
# Mock driver, connection, and cursor
76+
mock_driver = Mock()
77+
mock_conn = Mock()
78+
mock_cursor = Mock()
79+
80+
# Configure the mock chain
81+
mock_driver.prepare_connection.return_value = mock_conn
82+
mock_driver.create_connection.return_value = mock_conn
83+
mock_conn.cursor.return_value = mock_cursor
84+
mock_cursor.fetchall.return_value = [("test_data",)]
85+
86+
# Configure failures
87+
if cursor_fails:
88+
mock_cursor.close.side_effect = Exception("Cursor close failed")
89+
if conn_fails:
90+
mock_conn.close.side_effect = Exception("Connection close failed")
91+
92+
# Create mock driver class that returns our mock driver
93+
mock_driver_class = Mock(return_value=mock_driver)
94+
95+
# Create reader with the mock driver class
96+
reader = DataSourceReader(
97+
driver_class=mock_driver_class,
98+
create_connection=mock_create_connection,
99+
schema=expected_schema,
100+
dbms_type=DBMS_TYPE.UNKNOWN,
101+
fetch_size=0, # Use 0 to trigger fetchall() path
102+
query_timeout=0,
103+
session_init_statement=None,
104+
fetch_merge_count=1,
105+
)
106+
107+
# Test - should succeed despite close errors and log the failure
108+
with patch(
109+
"snowflake.snowpark._internal.data_source.datasource_reader.logger"
110+
) as mock_logger:
111+
# Consume the generator to trigger execution of the finally block
112+
list(reader.read("SELECT * FROM test"))
113+
114+
mock_logger.debug.assert_called()
115+
args, kwargs = mock_logger.debug.call_args
116+
assert re.search(r"Failed to close", args[0])

0 commit comments

Comments
 (0)