Skip to content

Commit 293e356

Browse files
Merge branch 'sea-migration' into metadata-sea
2 parents 59b1330 + 59b4825 commit 293e356

File tree

9 files changed

+113
-266
lines changed

9 files changed

+113
-266
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Release History
22

3+
# 4.0.5 (2025-06-24)
4+
- Fix: Reverted change in cursor close handling which led to errors impacting users (databricks/databricks-sql-python#613 by @madhav-db)
5+
36
# 4.0.4 (2025-06-16)
47

58
- Update thrift client library after cleaning up unused fields and structs (databricks/databricks-sql-python#553 by @vikrantpuppala)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "databricks-sql-connector"
3-
version = "4.0.4"
3+
version = "4.0.5"
44
description = "Databricks SQL Connector for Python"
55
authors = ["Databricks <databricks-sql-connector-maintainers@databricks.com>"]
66
license = "Apache-2.0"

src/databricks/sql/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def __repr__(self):
6868
DATE = DBAPITypeObject("date")
6969
ROWID = DBAPITypeObject()
7070

71-
__version__ = "4.0.4"
71+
__version__ = "4.0.5"
7272
USER_AGENT_NAME = "PyDatabricksSqlConnector"
7373

7474
# These two functions are pyhive legacy

src/databricks/sql/client.py

Lines changed: 3 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -281,13 +281,7 @@ def __enter__(self) -> "Connection":
281281
return self
282282

283283
def __exit__(self, exc_type, exc_value, traceback):
284-
try:
285-
self.close()
286-
except BaseException as e:
287-
logger.warning(f"Exception during connection close in __exit__: {e}")
288-
if exc_type is None:
289-
raise
290-
return False
284+
self.close()
291285

292286
def __del__(self):
293287
if self.open:
@@ -417,14 +411,7 @@ def __enter__(self) -> "Cursor":
417411
return self
418412

419413
def __exit__(self, exc_type, exc_value, traceback):
420-
try:
421-
logger.debug("Cursor context manager exiting, calling close()")
422-
self.close()
423-
except BaseException as e:
424-
logger.warning(f"Exception during cursor close in __exit__: {e}")
425-
if exc_type is None:
426-
raise
427-
return False
414+
self.close()
428415

429416
def __iter__(self):
430417
if self.active_result_set:
@@ -1091,21 +1078,7 @@ def cancel(self) -> None:
10911078
def close(self) -> None:
10921079
"""Close cursor"""
10931080
self.open = False
1094-
1095-
# Close active operation handle if it exists
1096-
if self.active_command_id:
1097-
try:
1098-
self.backend.close_command(self.active_command_id)
1099-
except RequestError as e:
1100-
if isinstance(e.args[1], CursorAlreadyClosedError):
1101-
logger.info("Operation was canceled by a prior request")
1102-
else:
1103-
logging.warning(f"Error closing operation handle: {e}")
1104-
except Exception as e:
1105-
logging.warning(f"Error closing operation handle: {e}")
1106-
finally:
1107-
self.active_command_id = None
1108-
1081+
self.active_command_id = None
11091082
if self.active_result_set:
11101083
self._close_and_clear_active_result_set()
11111084

tests/e2e/common/large_queries_mixin.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,11 @@ def test_query_with_large_narrow_result_set(self):
8383
assert row[0] == row_id
8484

8585
def test_long_running_query(self):
86-
"""Incrementally increase query size until it takes at least 5 minutes,
86+
"""Incrementally increase query size until it takes at least 3 minutes,
8787
and asserts that the query completes successfully.
8888
"""
8989
minutes = 60
90-
min_duration = 5 * minutes
90+
min_duration = 3 * minutes
9191

9292
duration = -1
9393
scale0 = 10000
@@ -113,5 +113,5 @@ def test_long_running_query(self):
113113
duration = time.time() - start
114114
current_fraction = duration / min_duration
115115
print("Took {} s with scale factor={}".format(duration, scale_factor))
116-
# Extrapolate linearly to reach 5 min and add 50% padding to push over the limit
116+
# Extrapolate linearly to reach 3 min and add 50% padding to push over the limit
117117
scale_factor = math.ceil(1.5 * scale_factor / current_fraction)

tests/e2e/common/staging_ingestion_tests.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def test_staging_ingestion_life_cycle(self, ingestion_user):
4646
) as conn:
4747

4848
cursor = conn.cursor()
49-
query = f"PUT '{temp_path}' INTO 'stage://tmp/{ingestion_user}/tmp/11/15/file1.csv' OVERWRITE"
49+
query = f"PUT '{temp_path}' INTO 'stage://tmp/{ingestion_user}/tmp/11/16/file1.csv' OVERWRITE"
5050
cursor.execute(query)
5151

5252
# GET should succeed
@@ -57,7 +57,7 @@ def test_staging_ingestion_life_cycle(self, ingestion_user):
5757
extra_params={"staging_allowed_local_path": new_temp_path}
5858
) as conn:
5959
cursor = conn.cursor()
60-
query = f"GET 'stage://tmp/{ingestion_user}/tmp/11/15/file1.csv' TO '{new_temp_path}'"
60+
query = f"GET 'stage://tmp/{ingestion_user}/tmp/11/16/file1.csv' TO '{new_temp_path}'"
6161
cursor.execute(query)
6262

6363
with open(new_fh, "rb") as fp:
@@ -67,7 +67,7 @@ def test_staging_ingestion_life_cycle(self, ingestion_user):
6767

6868
# REMOVE should succeed
6969

70-
remove_query = f"REMOVE 'stage://tmp/{ingestion_user}/tmp/11/15/file1.csv'"
70+
remove_query = f"REMOVE 'stage://tmp/{ingestion_user}/tmp/11/16/file1.csv'"
7171

7272
with self.connection(extra_params={"staging_allowed_local_path": "/"}) as conn:
7373
cursor = conn.cursor()
@@ -79,7 +79,7 @@ def test_staging_ingestion_life_cycle(self, ingestion_user):
7979
Error, match="Staging operation over HTTP was unsuccessful: 404"
8080
):
8181
cursor = conn.cursor()
82-
query = f"GET 'stage://tmp/{ingestion_user}/tmp/11/15/file1.csv' TO '{new_temp_path}'"
82+
query = f"GET 'stage://tmp/{ingestion_user}/tmp/11/16/file1.csv' TO '{new_temp_path}'"
8383
cursor.execute(query)
8484

8585
os.remove(temp_path)

tests/e2e/test_driver.py

Lines changed: 1 addition & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151

5252
from tests.e2e.common.uc_volume_tests import PySQLUCVolumeTestSuiteMixin
5353

54-
from databricks.sql.exc import SessionAlreadyClosedError, CursorAlreadyClosedError
54+
from databricks.sql.exc import SessionAlreadyClosedError
5555

5656
log = logging.getLogger(__name__)
5757

@@ -809,142 +809,6 @@ def test_catalogs_returns_arrow_table(self):
809809
results = cursor.fetchall_arrow()
810810
assert isinstance(results, pyarrow.Table)
811811

812-
def test_close_connection_closes_cursors(self):
813-
814-
from databricks.sql.thrift_api.TCLIService import ttypes
815-
816-
with self.connection() as conn:
817-
cursor = conn.cursor()
818-
cursor.execute(
819-
"SELECT id, id `id2`, id `id3` FROM RANGE(1000000) order by RANDOM()"
820-
)
821-
ars = cursor.active_result_set
822-
823-
# We must manually run this check because thrift_backend always forces `has_been_closed_server_side` to True
824-
# Cursor op state should be open before connection is closed
825-
status_request = ttypes.TGetOperationStatusReq(
826-
operationHandle=ars.command_id.to_thrift_handle(),
827-
getProgressUpdate=False,
828-
)
829-
op_status_at_server = ars.backend._client.GetOperationStatus(status_request)
830-
assert op_status_at_server.operationState != CommandState.CLOSED
831-
832-
conn.close()
833-
834-
# When connection closes, any cursor operations should no longer exist at the server
835-
with pytest.raises(SessionAlreadyClosedError) as cm:
836-
op_status_at_server = ars.backend._client.GetOperationStatus(
837-
status_request
838-
)
839-
840-
def test_closing_a_closed_connection_doesnt_fail(self, caplog):
841-
caplog.set_level(logging.DEBUG)
842-
# Second .close() call is when this context manager exits
843-
with self.connection() as conn:
844-
# First .close() call is explicit here
845-
conn.close()
846-
assert "Session appears to have been closed already" in caplog.text
847-
848-
conn = None
849-
try:
850-
with pytest.raises(KeyboardInterrupt):
851-
with self.connection() as c:
852-
conn = c
853-
raise KeyboardInterrupt("Simulated interrupt")
854-
finally:
855-
if conn is not None:
856-
assert (
857-
not conn.open
858-
), "Connection should be closed after KeyboardInterrupt"
859-
860-
def test_cursor_close_properly_closes_operation(self):
861-
"""Test that Cursor.close() properly closes the active operation handle on the server."""
862-
with self.connection() as conn:
863-
cursor = conn.cursor()
864-
try:
865-
cursor.execute("SELECT 1 AS test")
866-
assert cursor.active_command_id is not None
867-
cursor.close()
868-
assert cursor.active_command_id is None
869-
assert not cursor.open
870-
finally:
871-
if cursor.open:
872-
cursor.close()
873-
874-
conn = None
875-
cursor = None
876-
try:
877-
with self.connection() as c:
878-
conn = c
879-
with pytest.raises(KeyboardInterrupt):
880-
with conn.cursor() as cur:
881-
cursor = cur
882-
raise KeyboardInterrupt("Simulated interrupt")
883-
finally:
884-
if cursor is not None:
885-
assert (
886-
not cursor.open
887-
), "Cursor should be closed after KeyboardInterrupt"
888-
889-
def test_nested_cursor_context_managers(self):
890-
"""Test that nested cursor context managers properly close operations on the server."""
891-
with self.connection() as conn:
892-
with conn.cursor() as cursor1:
893-
cursor1.execute("SELECT 1 AS test1")
894-
assert cursor1.active_command_id is not None
895-
896-
with conn.cursor() as cursor2:
897-
cursor2.execute("SELECT 2 AS test2")
898-
assert cursor2.active_command_id is not None
899-
900-
# After inner context manager exit, cursor2 should be not open
901-
assert not cursor2.open
902-
assert cursor2.active_command_id is None
903-
904-
# After outer context manager exit, cursor1 should be not open
905-
assert not cursor1.open
906-
assert cursor1.active_command_id is None
907-
908-
def test_cursor_error_handling(self):
909-
"""Test that cursor close handles errors properly to prevent orphaned operations."""
910-
with self.connection() as conn:
911-
cursor = conn.cursor()
912-
913-
cursor.execute("SELECT 1 AS test")
914-
915-
op_handle = cursor.active_command_id
916-
917-
assert op_handle is not None
918-
919-
# Manually close the operation to simulate server-side closure
920-
conn.session.backend.close_command(op_handle)
921-
922-
cursor.close()
923-
924-
assert not cursor.open
925-
926-
def test_result_set_close(self):
927-
"""Test that ResultSet.close() properly closes operations on the server and handles state correctly."""
928-
with self.connection() as conn:
929-
cursor = conn.cursor()
930-
try:
931-
cursor.execute("SELECT * FROM RANGE(10)")
932-
933-
result_set = cursor.active_result_set
934-
assert result_set is not None
935-
936-
initial_op_state = result_set.status
937-
938-
result_set.close()
939-
940-
assert result_set.status == CommandState.CLOSED
941-
assert result_set.status != initial_op_state
942-
943-
# Closing the result set again should be a no-op and not raise exceptions
944-
result_set.close()
945-
finally:
946-
cursor.close()
947-
948812

949813
# use a RetrySuite to encapsulate these tests which we'll typically want to run together; however keep
950814
# the 429/503 subsuites separate since they execute under different circumstances.

0 commit comments

Comments
 (0)