Skip to content

Commit 14466cf

Browse files
test: focus async rowcount regression coverage
Signed-off-by: Aakash Saravanan <aakash.saravanan@databricks.com>
1 parent f06979f commit 14466cf

3 files changed

Lines changed: 72 additions & 147 deletions

File tree

‎KERNEL_REV‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
426993a6838aebb7d37b8fc89fa606b4897dd3f2
1+
453017f8c305e083e14b9b85183bc00920523ad5

‎tests/unit/test_kernel_client.py‎

Lines changed: 13 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1234,53 +1234,21 @@ def test_get_query_state_uses_reader_after_result_stream_started():
12341234

12351235

12361236
@pytest.mark.parametrize("count", [7, 0, None])
1237-
@pytest.mark.parametrize("source", ["owner", "reader", "attached"])
1238-
def test_async_result_count_after_await(source, count):
1237+
def test_get_execution_result_uses_retained_owning_handle_first(count):
1238+
"""The first in-process result fetch uses the retained submitting
1239+
handle so the kernel finalizes the original async statement telemetry."""
12391240
c = _make_client()
12401241
c._kernel_session = MagicMock()
1241-
cid = CommandId.from_sea_statement_id("async-count")
1242-
owner = MagicMock()
1243-
handle = owner
1244-
if source == "attached":
1245-
c._kernel_session.attach_async_statement.return_value = handle
1246-
else:
1247-
c._async_handles[cid.guid] = owner
1248-
if source == "reader":
1249-
c._async_result_stream_started.add(cid.guid)
1250-
handle = owner.reader.return_value
1251-
1242+
fake_stream = MagicMock()
1243+
fake_stream.arrow_schema.return_value = pa.schema([("n", pa.int64())])
1244+
handle = MagicMock()
12521245
del handle.num_modified_rows
1253-
stream = MagicMock()
1254-
stream.arrow_schema.return_value = pa.schema([("num_affected_rows", pa.int64())])
1255-
stream.fetch_next_batch.side_effect = [
1256-
pa.record_batch(
1257-
[[count if count is not None else 1]], names=["num_affected_rows"]
1258-
),
1259-
None,
1260-
]
12611246

12621247
def await_result():
12631248
handle.num_modified_rows = count
1264-
return stream
1249+
return fake_stream
12651250

12661251
handle.await_result.side_effect = await_result
1267-
cursor = MagicMock(arraysize=100, buffer_size_bytes=1024)
1268-
rs = c.get_execution_result(cid, cursor)
1269-
1270-
assert rs.num_modified_rows == count
1271-
stream.fetch_next_batch.assert_not_called()
1272-
assert [list(row) for row in rs.fetchall()] == [[count if count is not None else 1]]
1273-
1274-
1275-
def test_get_execution_result_uses_retained_owning_handle_first():
1276-
"""The first in-process result fetch uses the retained submitting
1277-
handle so the kernel finalizes the original async statement telemetry."""
1278-
c = _make_client()
1279-
c._kernel_session = MagicMock()
1280-
fake_stream = MagicMock()
1281-
fake_stream.arrow_schema.return_value = pa.schema([("n", pa.int64())])
1282-
handle = MagicMock()
1283-
handle.await_result.return_value = fake_stream
12841252
cursor = MagicMock()
12851253
cursor.arraysize = 100
12861254
cursor.buffer_size_bytes = 1024
@@ -1291,6 +1259,8 @@ def test_get_execution_result_uses_retained_owning_handle_first():
12911259
rs = c.get_execution_result(cid, cursor=cursor)
12921260

12931261
assert rs is not None
1262+
assert rs.num_modified_rows == count
1263+
fake_stream.fetch_next_batch.assert_not_called()
12941264
c._kernel_session.attach_async_statement.assert_not_called()
12951265
handle.await_result.assert_called_once_with()
12961266
assert cid.guid in c._async_result_stream_started
@@ -1303,6 +1273,7 @@ def test_get_execution_result_attaches_by_id_when_no_retained_handle():
13031273
fake_stream = MagicMock()
13041274
fake_stream.arrow_schema.return_value = pa.schema([("n", pa.int64())])
13051275
handle = _attach_returns(c, await_result=fake_stream)
1276+
handle.num_modified_rows = 7
13061277
cursor = MagicMock()
13071278
cursor.arraysize = 100
13081279
cursor.buffer_size_bytes = 1024
@@ -1311,6 +1282,7 @@ def test_get_execution_result_attaches_by_id_when_no_retained_handle():
13111282
rs = c.get_execution_result(cid, cursor=cursor)
13121283

13131284
assert rs is not None
1285+
assert rs.num_modified_rows == 7
13141286
c._kernel_session.attach_async_statement.assert_called_once_with("async-1")
13151287
handle.await_result.assert_called_once_with()
13161288

@@ -1365,6 +1337,7 @@ def test_get_execution_result_construction_failure_retains_marker_and_uses_reade
13651337
retry_stream.arrow_schema.return_value = pa.schema([("n", pa.int64())])
13661338
reader = MagicMock()
13671339
reader.await_result.return_value = retry_stream
1340+
reader.num_modified_rows = 0
13681341
owning_handle.reader.return_value = reader
13691342
cursor = MagicMock()
13701343
cursor.arraysize = 100
@@ -1373,6 +1346,7 @@ def test_get_execution_result_construction_failure_retains_marker_and_uses_reade
13731346
rs = c.get_execution_result(cid, cursor=cursor)
13741347

13751348
assert rs is not None
1349+
assert rs.num_modified_rows == 0
13761350
owning_handle.reader.assert_called_once_with()
13771351
c._kernel_session.attach_async_statement.assert_not_called()
13781352
reader.await_result.assert_called_once_with()

‎tests/unit/test_thrift_backend.py‎

Lines changed: 58 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -134,113 +134,6 @@ def test_metadata_catalog_escaping(
134134
assert getattr(request, thrift_field) == value
135135

136136

137-
@pytest.mark.skipif(pyarrow is None, reason="PyArrow is not installed")
138-
@pytest.mark.parametrize(
139-
"operation_state,count,expected_count",
140-
[
141-
(ttypes.TOperationState.FINISHED_STATE, 7, 7),
142-
(ttypes.TOperationState.FINISHED_STATE, 0, 0),
143-
(ttypes.TOperationState.FINISHED_STATE, None, None),
144-
(ttypes.TOperationState.RUNNING_STATE, 7, None),
145-
],
146-
)
147-
@pytest.mark.parametrize("cache_status", [True, False])
148-
def test_async_result_reads_only_terminal_status_count_and_preserves_rows(
149-
operation_state, count, expected_count, cache_status
150-
):
151-
with (
152-
patch(
153-
"databricks.sql.backend.thrift_backend.TCLIService.Client", autospec=True
154-
),
155-
patch("databricks.sql.backend.thrift_backend.ThriftResultSet") as result_set,
156-
):
157-
backend = ThriftDatabricksClient(
158-
"foobar",
159-
443,
160-
"path",
161-
[],
162-
auth_provider=AuthProvider(),
163-
ssl_options=SSLOptions(),
164-
http_client=MagicMock(),
165-
)
166-
operation_status = ttypes.TGetOperationStatusResp(
167-
status=ThriftBackendTestSuite.okay_status,
168-
operationState=operation_state,
169-
numModifiedRows=count,
170-
)
171-
backend._poll_for_status = Mock(return_value=operation_status)
172-
metadata = ttypes.TGetResultSetMetadataResp(
173-
schema=ttypes.TTableSchema(columns=[]),
174-
arrowSchema=pyarrow.schema([]).serialize().to_pybytes(),
175-
resultFormat=ttypes.TSparkRowSetType.ARROW_BASED_SET,
176-
)
177-
backend.make_request = Mock(
178-
return_value=Mock(
179-
resultSetMetadata=metadata,
180-
results=sentinel.counter_rows,
181-
hasMoreRows=False,
182-
)
183-
)
184-
cid = CommandId.from_thrift_handle(ThriftBackendTestSuite.operation_handle)
185-
if cache_status:
186-
expected_state = (
187-
CommandState.SUCCEEDED
188-
if operation_state == ttypes.TOperationState.FINISHED_STATE
189-
else CommandState.RUNNING
190-
)
191-
assert backend.get_query_state(cid) == expected_state
192-
backend.get_execution_result(cid, Mock())
193-
194-
expected_polls = (
195-
2
196-
if cache_status and operation_state == ttypes.TOperationState.RUNNING_STATE
197-
else 1
198-
)
199-
assert backend._poll_for_status.call_count == expected_polls
200-
backend._poll_for_status.assert_called_with(cid.to_thrift_handle())
201-
response = result_set.call_args.kwargs["execute_response"]
202-
assert response.num_modified_rows == expected_count
203-
assert result_set.call_args.kwargs["t_row_set"] is sentinel.counter_rows
204-
205-
206-
@pytest.mark.parametrize(
207-
"operation_state,exception_type",
208-
[
209-
(ttypes.TOperationState.ERROR_STATE, ServerOperationError),
210-
(ttypes.TOperationState.CLOSED_STATE, DatabaseError),
211-
],
212-
)
213-
def test_async_result_rejects_terminal_error_state_before_fetch(
214-
operation_state, exception_type
215-
):
216-
with patch(
217-
"databricks.sql.backend.thrift_backend.TCLIService.Client", autospec=True
218-
):
219-
backend = ThriftDatabricksClient(
220-
"foobar",
221-
443,
222-
"path",
223-
[],
224-
auth_provider=AuthProvider(),
225-
ssl_options=SSLOptions(),
226-
http_client=MagicMock(),
227-
)
228-
backend._poll_for_status = Mock(
229-
return_value=ttypes.TGetOperationStatusResp(
230-
status=ThriftBackendTestSuite.okay_status,
231-
operationState=operation_state,
232-
errorMessage="async operation failed",
233-
)
234-
)
235-
backend.make_request = Mock()
236-
cid = CommandId.from_thrift_handle(ThriftBackendTestSuite.operation_handle)
237-
238-
with pytest.raises(exception_type):
239-
backend.get_execution_result(cid, Mock())
240-
241-
backend.make_request.assert_not_called()
242-
243-
244137
@pytest.mark.skipif(pyarrow is None, reason="PyArrow is not installed")
245138
class ThriftBackendTestSuite(unittest.TestCase):
246139
okay_status = ttypes.TStatus(statusCode=ttypes.TStatusCode.SUCCESS_STATUS)
@@ -320,6 +213,64 @@ def _make_fake_thrift_backend(self):
320213
thrift_backend._create_arrow_table.return_value = (MagicMock(), Mock())
321214
return thrift_backend
322215

216+
@patch("databricks.sql.backend.thrift_backend.ThriftResultSet")
217+
@patch("databricks.sql.backend.thrift_backend.TCLIService.Client", autospec=True)
218+
def test_async_result_rowcount(self, tcli_service_class, result_set):
219+
for state, count, expected_count, polled in [
220+
(ttypes.TOperationState.FINISHED_STATE, 7, 7, True),
221+
(ttypes.TOperationState.FINISHED_STATE, 0, 0, True),
222+
(ttypes.TOperationState.FINISHED_STATE, None, None, True),
223+
(ttypes.TOperationState.FINISHED_STATE, 7, 7, False),
224+
(ttypes.TOperationState.RUNNING_STATE, 7, None, False),
225+
]:
226+
with self.subTest(state=state, count=count, polled=polled):
227+
backend = self._make_fake_thrift_backend()
228+
backend._poll_for_status = Mock(
229+
return_value=ttypes.TGetOperationStatusResp(
230+
status=self.okay_status,
231+
operationState=state,
232+
numModifiedRows=count,
233+
)
234+
)
235+
backend.make_request = Mock(
236+
return_value=Mock(
237+
resultSetMetadata=self.metadata_resp,
238+
results=sentinel.counter_rows,
239+
hasMoreRows=False,
240+
)
241+
)
242+
cid = CommandId.from_thrift_handle(self.operation_handle)
243+
if polled:
244+
self.assertEqual(
245+
backend.get_query_state(cid), CommandState.SUCCEEDED
246+
)
247+
backend.get_execution_result(cid, Mock())
248+
249+
backend._poll_for_status.assert_called_once_with(cid.to_thrift_handle())
250+
response = result_set.call_args.kwargs["execute_response"]
251+
self.assertEqual(response.num_modified_rows, expected_count)
252+
self.assertIs(
253+
result_set.call_args.kwargs["t_row_set"], sentinel.counter_rows
254+
)
255+
256+
@patch("databricks.sql.backend.thrift_backend.TCLIService.Client", autospec=True)
257+
def test_async_result_checks_status_before_fetch(self, tcli_service_class):
258+
backend = self._make_fake_thrift_backend()
259+
backend._poll_for_status = Mock(
260+
return_value=ttypes.TGetOperationStatusResp(
261+
status=self.okay_status,
262+
operationState=ttypes.TOperationState.ERROR_STATE,
263+
errorMessage="async operation failed",
264+
)
265+
)
266+
backend.make_request = Mock()
267+
cid = CommandId.from_thrift_handle(self.operation_handle)
268+
269+
with self.assertRaises(ServerOperationError):
270+
backend.get_execution_result(cid, Mock())
271+
272+
backend.make_request.assert_not_called()
273+
323274
def test_hive_schema_to_arrow_schema_preserves_column_names(self):
324275
columns = [
325276
ttypes.TColumnDesc(

0 commit comments

Comments
 (0)