Skip to content

Commit f06979f

Browse files
fix: read async affected-row counts directly
Signed-off-by: Aakash Saravanan <aakash.saravanan@databricks.com>
1 parent 1dee683 commit f06979f

4 files changed

Lines changed: 17 additions & 42 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
# Unreleased
44
- Fix inline parameters (`use_inline_params=True`) so a sequence directly after `IN` renders as a list again: `x IN %(ids)s` renders as `x IN (1,2,3)` instead of `x IN ARRAY(1,2,3)`, which Spark rejects. Inside the list, tuples render as rows for a multi-column `IN`, and a `set` or `frozenset` is accepted again. Sequences anywhere else still render as `ARRAY(...)` ([#629](https://github.com/databricks/databricks-sql-python/issues/629))
5-
- Report `cursor.rowcount` after asynchronous DML completes. With `use_kernel=True`, this requires a kernel release containing async affected-row count support; kernel 1.4.0 continues to report `-1`.
5+
- Report `cursor.rowcount` after asynchronous DML completes on both the Thrift and kernel backends, preserving zero counts and fetched result rows.
66

77
# 4.9.0 (2026-11-04)
88
- Upgrade Databricks SQL Kernel to 1.4.0, which keeps Lakehouse Real-Time session affinity and flushes shared telemetry only after the final session closes.

‎src/databricks/sql/backend/kernel/client.py‎

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -138,14 +138,6 @@ def _async_statement_reader(owner: Any) -> Any:
138138
return reader()
139139

140140

141-
def _read_num_modified_rows(handle: Any) -> Optional[int]:
142-
try:
143-
modified = getattr(handle, "num_modified_rows", None)
144-
return modified() if callable(modified) else modified
145-
except Exception:
146-
return None
147-
148-
149141
def _catalog_or_none(value: Optional[str]) -> Optional[str]:
150142
"""Map supported all-catalog wildcards to the kernel's unset filter."""
151143
if value is None or value in ("%", "*"):
@@ -699,7 +691,12 @@ def execute_command(
699691
# default. ``getattr`` guards against an older kernel wheel that
700692
# predates the pyo3 getter. NB the Thrift backend also hardcodes
701693
# ``-1`` here, so this makes the kernel path *exceed* Thrift.
702-
modified = _read_num_modified_rows(executed)
694+
try:
695+
modified = getattr(executed, "num_modified_rows", None)
696+
if callable(modified):
697+
modified = modified()
698+
except Exception:
699+
modified = None
703700
if modified is not None:
704701
cursor.rowcount = modified
705702
# ``KernelResultSet.__init__`` calls ``arrow_schema()`` which
@@ -970,9 +967,7 @@ def get_execution_result(
970967
# result-set construction fails after a successful await.
971968
try:
972969
result_set = self._make_result_set(stream, cursor, command_id)
973-
# Older kernel wheels do not expose this value. Keep them compatible
974-
# while allowing a newer binding to populate async rowcount.
975-
result_set.num_modified_rows = _read_num_modified_rows(handle)
970+
result_set.num_modified_rows = handle.num_modified_rows
976971
return result_set
977972
except Exception as exc:
978973
raise _wrap_kernel_exception("get_execution_result", exc) from exc

‎src/databricks/sql/client.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1714,8 +1714,10 @@ def get_async_execution_result(self):
17141714
self.active_command_id, self
17151715
)
17161716

1717-
num_modified_rows = getattr(
1718-
self.active_result_set, "num_modified_rows", None
1717+
num_modified_rows = (
1718+
self.active_result_set.num_modified_rows
1719+
if self.active_result_set
1720+
else None
17191721
)
17201722
self.rowcount = num_modified_rows if num_modified_rows is not None else -1
17211723

‎tests/unit/test_kernel_client.py‎

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1233,26 +1233,7 @@ def test_get_query_state_uses_reader_after_result_stream_started():
12331233
reader.status.assert_called_once_with()
12341234

12351235

1236-
class _RaisingModifiedRows:
1237-
@property
1238-
def num_modified_rows(self):
1239-
raise RuntimeError("unavailable")
1240-
1241-
1242-
@pytest.mark.parametrize(
1243-
("handle", "expected"),
1244-
[
1245-
(types.SimpleNamespace(num_modified_rows=7), 7),
1246-
(types.SimpleNamespace(num_modified_rows=lambda: 8), 8),
1247-
(types.SimpleNamespace(), None),
1248-
(_RaisingModifiedRows(), None),
1249-
],
1250-
)
1251-
def test_read_num_modified_rows_compatibility_shapes(handle, expected):
1252-
assert kernel_client._read_num_modified_rows(handle) == expected
1253-
1254-
1255-
@pytest.mark.parametrize("count", [7, 0, None, "unavailable"])
1236+
@pytest.mark.parametrize("count", [7, 0, None])
12561237
@pytest.mark.parametrize("source", ["owner", "reader", "attached"])
12571238
def test_async_result_count_after_await(source, count):
12581239
c = _make_client()
@@ -1273,25 +1254,22 @@ def test_async_result_count_after_await(source, count):
12731254
stream.arrow_schema.return_value = pa.schema([("num_affected_rows", pa.int64())])
12741255
stream.fetch_next_batch.side_effect = [
12751256
pa.record_batch(
1276-
[[count if isinstance(count, int) else 1]], names=["num_affected_rows"]
1257+
[[count if count is not None else 1]], names=["num_affected_rows"]
12771258
),
12781259
None,
12791260
]
12801261

12811262
def await_result():
1282-
if count != "unavailable":
1283-
handle.num_modified_rows = count
1263+
handle.num_modified_rows = count
12841264
return stream
12851265

12861266
handle.await_result.side_effect = await_result
12871267
cursor = MagicMock(arraysize=100, buffer_size_bytes=1024)
12881268
rs = c.get_execution_result(cid, cursor)
12891269

1290-
assert rs.num_modified_rows == (None if count == "unavailable" else count)
1270+
assert rs.num_modified_rows == count
12911271
stream.fetch_next_batch.assert_not_called()
1292-
assert [list(row) for row in rs.fetchall()] == [
1293-
[count if isinstance(count, int) else 1]
1294-
]
1272+
assert [list(row) for row in rs.fetchall()] == [[count if count is not None else 1]]
12951273

12961274

12971275
def test_get_execution_result_uses_retained_owning_handle_first():

0 commit comments

Comments
 (0)