This is an automated email from the ASF dual-hosted git repository.
gopidesu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 40a3c7e21ca feat: Add new query related methods to SnowflakeSqlApiHook
(#52157)
40a3c7e21ca is described below
commit 40a3c7e21ca9d06c3ec755542b8af0f11f8bf127
Author: Kacper Muda <[email protected]>
AuthorDate: Fri Jun 27 12:31:55 2025 +0200
feat: Add new query related methods to SnowflakeSqlApiHook (#52157)
---
.../providers/snowflake/hooks/snowflake_sql_api.py | 75 +++++
.../unit/snowflake/hooks/test_snowflake_sql_api.py | 331 ++++++++++++++++-----
2 files changed, 337 insertions(+), 69 deletions(-)
diff --git
a/providers/snowflake/src/airflow/providers/snowflake/hooks/snowflake_sql_api.py
b/providers/snowflake/src/airflow/providers/snowflake/hooks/snowflake_sql_api.py
index 185a49ffaba..d87e4cd04d3 100644
---
a/providers/snowflake/src/airflow/providers/snowflake/hooks/snowflake_sql_api.py
+++
b/providers/snowflake/src/airflow/providers/snowflake/hooks/snowflake_sql_api.py
@@ -17,6 +17,7 @@
from __future__ import annotations
import base64
+import time
import uuid
import warnings
from datetime import timedelta
@@ -313,6 +314,80 @@ class SnowflakeSqlApiHook(SnowflakeHook):
status_code, resp = self._make_api_call_with_retries("GET", url,
header, params)
return self._process_response(status_code, resp)
+ def wait_for_query(
+ self, query_id: str, raise_error: bool = False, poll_interval: int =
5, timeout: int = 60
+ ) -> dict[str, str | list[str]]:
+ """
+ Wait for query to finish either successfully or with error.
+
+ :param query_id: statement handle id for the individual statement.
+ :param raise_error: whether to raise an error if the query failed.
+ :param poll_interval: time (in seconds) between checking the query
status.
+ :param timeout: max time (in seconds) to wait for the query to finish
before raising a TimeoutError.
+
+ :raises RuntimeError: If the query status is 'error' and `raise_error`
is True.
+ :raises TimeoutError: If the query doesn't finish within the specified
timeout.
+ """
+ start_time = time.time()
+
+ while True:
+ response = self.get_sql_api_query_status(query_id=query_id)
+ self.log.debug("Query status `%s`", response["status"])
+
+ if time.time() - start_time > timeout:
+ raise TimeoutError(
+ f"Query `{query_id}` did not finish within the timeout
period of {timeout} seconds."
+ )
+
+ if response["status"] != "running":
+ self.log.info("Query status `%s`", response["status"])
+ break
+
+ time.sleep(poll_interval)
+
+ if response["status"] == "error" and raise_error:
+ raise RuntimeError(response["message"])
+
+ return response
+
+ def get_result_from_successful_sql_api_query(self, query_id: str) ->
list[dict[str, Any]]:
+ """
+ Based on the query id HTTP requests are made to snowflake SQL API and
return result data.
+
+ :param query_id: statement handle id for the individual statement.
+
+ :raises RuntimeError: If the query status is not 'success'.
+ """
+ self.log.info("Retrieving data for query id %s", query_id)
+ header, params, url = self.get_request_url_header_params(query_id)
+ status_code, response = self._make_api_call_with_retries("GET", url,
header, params)
+
+ if (query_status := self._process_response(status_code,
response)["status"]) != "success":
+ msg = f"Query must have status `success` to retrieve data; got
`{query_status}`."
+ raise RuntimeError(msg)
+
+ # Below fields should always be present in response, but added some
safety checks
+ data = response.get("data", [])
+ if not data:
+ self.log.warning("No data found in the API response.")
+ return []
+ metadata = response.get("resultSetMetaData", {})
+ col_names = [row["name"] for row in metadata.get("rowType", [])]
+ if not col_names:
+ self.log.warning("No column metadata found in the API response.")
+ return []
+
+ num_partitions = len(metadata.get("partitionInfo", []))
+ if num_partitions > 1:
+ self.log.debug("Result data is returned as multiple partitions.
Will perform additional queries.")
+ url += "?partition="
+ for partition_no in range(1, num_partitions): # First partition
was already returned
+ self.log.debug("Querying for partition no. %s", partition_no)
+ _, response = self._make_api_call_with_retries("GET", url +
str(partition_no), header, params)
+ data.extend(response.get("data", []))
+
+ return [dict(zip(col_names, row)) for row in data] # Merged column
names with data
+
async def get_sql_api_query_status_async(self, query_id: str) -> dict[str,
str | list[str]]:
"""
Based on the query id async HTTP request is made to snowflake SQL API
and return response.
diff --git
a/providers/snowflake/tests/unit/snowflake/hooks/test_snowflake_sql_api.py
b/providers/snowflake/tests/unit/snowflake/hooks/test_snowflake_sql_api.py
index 8e610e2fd75..2cbf156a2c9 100644
--- a/providers/snowflake/tests/unit/snowflake/hooks/test_snowflake_sql_api.py
+++ b/providers/snowflake/tests/unit/snowflake/hooks/test_snowflake_sql_api.py
@@ -33,9 +33,7 @@ from cryptography.hazmat.primitives.asymmetric import rsa
from airflow.exceptions import AirflowException,
AirflowProviderDeprecationWarning
from airflow.models import Connection
-from airflow.providers.snowflake.hooks.snowflake_sql_api import (
- SnowflakeSqlApiHook,
-)
+from airflow.providers.snowflake.hooks.snowflake_sql_api import
SnowflakeSqlApiHook
if TYPE_CHECKING:
from pathlib import Path
@@ -152,12 +150,13 @@ HOOK_PARAMS: dict = {
API_URL = "https://test.snowflakecomputing.com/api/v2/statements/test"
+MODULE_PATH = "airflow.providers.snowflake.hooks.snowflake_sql_api"
+HOOK_PATH = f"{MODULE_PATH}.SnowflakeSqlApiHook"
+
@pytest.fixture
def mock_requests():
- with mock.patch(
- "airflow.providers.snowflake.hooks.snowflake_sql_api.requests.Session"
- ) as mock_session_cls:
+ with mock.patch(f"{MODULE_PATH}.requests.Session") as mock_session_cls:
mock_session = mock.MagicMock()
mock_session_cls.return_value.__enter__.return_value = mock_session
yield mock_session
@@ -165,9 +164,7 @@ def mock_requests():
@pytest.fixture
def mock_async_request():
- with mock.patch(
-
"airflow.providers.snowflake.hooks.snowflake_sql_api.aiohttp.ClientSession.request"
- ) as mock_session_cls:
+ with mock.patch(f"{MODULE_PATH}.aiohttp.ClientSession.request") as
mock_session_cls:
mock_request = mock.MagicMock()
mock_session_cls.return_value = mock_request
yield mock_request
@@ -227,11 +224,8 @@ class TestSnowflakeSqlApiHook:
(SQL_MULTIPLE_STMTS, 4, {"statementHandles": ["uuid", "uuid1"]},
["uuid", "uuid1"]),
],
)
- @mock.patch(
-
"airflow.providers.snowflake.hooks.snowflake_sql_api.SnowflakeSqlApiHook._get_conn_params",
- new_callable=PropertyMock,
- )
-
@mock.patch("airflow.providers.snowflake.hooks.snowflake_sql_api.SnowflakeSqlApiHook.get_headers")
+ @mock.patch(f"{HOOK_PATH}._get_conn_params", new_callable=PropertyMock)
+ @mock.patch(f"{HOOK_PATH}.get_headers")
def test_execute_query(
self,
mock_get_header,
@@ -254,11 +248,8 @@ class TestSnowflakeSqlApiHook:
query_ids = hook.execute_query(sql, statement_count)
assert query_ids == expected_query_ids
- @mock.patch(
-
"airflow.providers.snowflake.hooks.snowflake_sql_api.SnowflakeSqlApiHook._get_conn_params",
- new_callable=PropertyMock,
- )
-
@mock.patch("airflow.providers.snowflake.hooks.snowflake_sql_api.SnowflakeSqlApiHook.get_headers")
+ @mock.patch(f"{HOOK_PATH}._get_conn_params", new_callable=PropertyMock)
+ @mock.patch(f"{HOOK_PATH}.get_headers")
def test_execute_query_multiple_times_give_fresh_query_ids_each_time(
self, mock_get_header, mock_conn_param, mock_requests
):
@@ -297,11 +288,8 @@ class TestSnowflakeSqlApiHook:
"sql,statement_count,expected_response, expected_query_ids",
[(SINGLE_STMT, 1, {"statementHandle": "uuid"}, ["uuid"])],
)
- @mock.patch(
-
"airflow.providers.snowflake.hooks.snowflake_sql_api.SnowflakeSqlApiHook._get_conn_params",
- new_callable=PropertyMock,
- )
-
@mock.patch("airflow.providers.snowflake.hooks.snowflake_sql_api.SnowflakeSqlApiHook.get_headers")
+ @mock.patch(f"{HOOK_PATH}._get_conn_params", new_callable=PropertyMock)
+ @mock.patch(f"{HOOK_PATH}.get_headers")
def test_execute_query_exception_without_statement_handle(
self,
mock_get_header,
@@ -330,11 +318,8 @@ class TestSnowflakeSqlApiHook:
(SQL_MULTIPLE_STMTS, 4, {"1": {"type": "FIXED", "value": "123"}}),
],
)
- @mock.patch(
-
"airflow.providers.snowflake.hooks.snowflake_sql_api.SnowflakeSqlApiHook._get_conn_params",
- new_callable=PropertyMock,
- )
-
@mock.patch("airflow.providers.snowflake.hooks.snowflake_sql_api.SnowflakeSqlApiHook.get_headers")
+ @mock.patch(f"{HOOK_PATH}._get_conn_params", new_callable=PropertyMock)
+ @mock.patch(f"{HOOK_PATH}.get_headers")
def test_execute_query_bindings_warning(
self,
mock_get_headers,
@@ -364,10 +349,7 @@ class TestSnowflakeSqlApiHook:
(["uuid", "uuid1"]),
],
)
- @mock.patch(
-
"airflow.providers.snowflake.hooks.snowflake_sql_api.SnowflakeSqlApiHook."
- "get_request_url_header_params"
- )
+ @mock.patch(f"{HOOK_PATH}.get_request_url_header_params")
def test_check_query_output(self, mock_geturl_header_params, query_ids,
mock_requests):
"""Test check_query_output by passing query ids as params and mock
get_request_url_header_params"""
req_id = uuid.uuid4()
@@ -380,10 +362,7 @@ class TestSnowflakeSqlApiHook:
mock_log_info.assert_called_with(GET_RESPONSE)
@pytest.mark.parametrize("query_ids", [["uuid", "uuid1"]])
- @mock.patch(
-
"airflow.providers.snowflake.hooks.snowflake_sql_api.SnowflakeSqlApiHook."
- "get_request_url_header_params"
- )
+ @mock.patch(f"{HOOK_PATH}.get_request_url_header_params")
def test_check_query_output_exception(
self,
mock_geturl_header_params,
@@ -405,11 +384,8 @@ class TestSnowflakeSqlApiHook:
with pytest.raises(requests.exceptions.HTTPError):
hook.check_query_output(query_ids)
- @mock.patch(
-
"airflow.providers.snowflake.hooks.snowflake_sql_api.SnowflakeSqlApiHook._get_conn_params",
- new_callable=PropertyMock,
- )
-
@mock.patch("airflow.providers.snowflake.hooks.snowflake_sql_api.SnowflakeSqlApiHook.get_headers")
+ @mock.patch(f"{HOOK_PATH}._get_conn_params", new_callable=PropertyMock)
+ @mock.patch(f"{HOOK_PATH}.get_headers")
def test_get_request_url_header_params(self, mock_get_header,
mock_conn_param):
"""Test get_request_url_header_params by mocking _get_conn_params and
get_headers"""
mock_conn_param.return_value = CONN_PARAMS
@@ -419,11 +395,8 @@ class TestSnowflakeSqlApiHook:
assert header == HEADERS
assert url ==
"https://airflow.af_region.snowflakecomputing.com/api/v2/statements/uuid"
-
@mock.patch("airflow.providers.snowflake.hooks.snowflake_sql_api.SnowflakeSqlApiHook.get_private_key")
- @mock.patch(
-
"airflow.providers.snowflake.hooks.snowflake_sql_api.SnowflakeSqlApiHook._get_conn_params",
- new_callable=PropertyMock,
- )
+ @mock.patch(f"{HOOK_PATH}.get_private_key")
+ @mock.patch(f"{HOOK_PATH}._get_conn_params", new_callable=PropertyMock)
@mock.patch("airflow.providers.snowflake.utils.sql_api_generate_jwt.JWTGenerator.get_token")
def test_get_headers_should_support_private_key(self, mock_get_token,
mock_conn_param, mock_private_key):
"""Test get_headers method by mocking get_private_key and
_get_conn_params method"""
@@ -433,11 +406,8 @@ class TestSnowflakeSqlApiHook:
result = hook.get_headers()
assert result == HEADERS
-
@mock.patch("airflow.providers.snowflake.hooks.snowflake_sql_api.SnowflakeSqlApiHook.get_oauth_token")
- @mock.patch(
-
"airflow.providers.snowflake.hooks.snowflake_sql_api.SnowflakeSqlApiHook._get_conn_params",
- new_callable=PropertyMock,
- )
+ @mock.patch(f"{HOOK_PATH}.get_oauth_token")
+ @mock.patch(f"{HOOK_PATH}._get_conn_params", new_callable=PropertyMock)
def test_get_headers_should_support_oauth(self, mock_conn_param,
mock_oauth_token):
"""Test get_headers method by mocking get_oauth_token and
_get_conn_params method"""
mock_conn_param.return_value = CONN_PARAMS_OAUTH
@@ -448,10 +418,7 @@ class TestSnowflakeSqlApiHook:
@mock.patch("airflow.providers.snowflake.hooks.snowflake.HTTPBasicAuth")
@mock.patch("requests.post")
- @mock.patch(
-
"airflow.providers.snowflake.hooks.snowflake_sql_api.SnowflakeSqlApiHook._get_conn_params",
- new_callable=PropertyMock,
- )
+ @mock.patch(f"{HOOK_PATH}._get_conn_params", new_callable=PropertyMock)
def test_get_oauth_token(self, mock_conn_param, requests_post, mock_auth):
"""Test get_oauth_token method makes the right http request"""
basic_auth = {"Authorization": "Basic usernamepassword"}
@@ -655,10 +622,7 @@ class TestSnowflakeSqlApiHook:
(404, {"status": "error", "message": "test"}, {"status": "error",
"message": "test"}),
],
)
- @mock.patch(
-
"airflow.providers.snowflake.hooks.snowflake_sql_api.SnowflakeSqlApiHook."
- "get_request_url_header_params"
- )
+ @mock.patch(f"{HOOK_PATH}.get_request_url_header_params")
def test_get_sql_api_query_status(
self, mock_geturl_header_params, status_code, response,
expected_response, mock_requests
):
@@ -718,10 +682,7 @@ class TestSnowflakeSqlApiHook:
(404, {"status": "error", "message": "test"}, {"status": "error",
"message": "test"}),
],
)
- @mock.patch(
-
"airflow.providers.snowflake.hooks.snowflake_sql_api.SnowflakeSqlApiHook."
- "get_request_url_header_params"
- )
+ @mock.patch(f"{HOOK_PATH}.get_request_url_header_params")
async def test_get_sql_api_query_status_async(
self, mock_geturl_header_params, status_code, response,
expected_response, mock_async_request
):
@@ -829,11 +790,8 @@ class TestSnowflakeSqlApiHook:
],
)
@mock.patch("uuid.uuid4")
- @mock.patch(
-
"airflow.providers.snowflake.hooks.snowflake_sql_api.SnowflakeSqlApiHook._get_conn_params",
- new_callable=PropertyMock,
- )
-
@mock.patch("airflow.providers.snowflake.hooks.snowflake_sql_api.SnowflakeSqlApiHook.get_headers")
+ @mock.patch(f"{HOOK_PATH}._get_conn_params", new_callable=PropertyMock)
+ @mock.patch(f"{HOOK_PATH}.get_headers")
def test_proper_parametrization_of_execute_query_api_request(
self,
mock_get_headers,
@@ -1079,6 +1037,241 @@ class TestSnowflakeSqlApiHook:
# Should attempt only 2 times due to custom config
assert mock_requests.request.call_count == 2
+ @mock.patch(f"{MODULE_PATH}.time.sleep")
+ def test_wait_for_query_immediate_success(self, sleep_mock):
+ hook = SnowflakeSqlApiHook(snowflake_conn_id="test_conn")
+ hook.get_sql_api_query_status = mock.MagicMock(
+ return_value={"status": "success", "results": ["row1", "row2"]}
+ )
+
+ result = hook.wait_for_query(query_id="qid-123")
+
+ assert result == {"status": "success", "results": ["row1", "row2"]}
+ sleep_mock.assert_not_called()
+
+ @mock.patch(f"{MODULE_PATH}.time.sleep")
+ def test_wait_for_query_polls_until_done(self, sleep_mock):
+ hook = SnowflakeSqlApiHook(snowflake_conn_id="test_conn")
+ hook.get_sql_api_query_status = mock.MagicMock(
+ side_effect=[
+ {"status": "running"},
+ {"status": "running"},
+ {"status": "success", "data": [1, 2, 3]},
+ ]
+ )
+ qid = "qid-456"
+
+ result = hook.wait_for_query(query_id=qid, poll_interval=2)
+
+ assert result == {"status": "success", "data": [1, 2, 3]}
+ assert sleep_mock.call_count == 2
+ sleep_mock.assert_has_calls([mock.call(2), mock.call(2)])
+ hook.get_sql_api_query_status.assert_has_calls(
+ [mock.call(query_id=qid), mock.call(query_id=qid),
mock.call(query_id=qid)]
+ )
+
+ @mock.patch(f"{MODULE_PATH}.time.sleep")
+ def test_wait_for_query_error_raises_when_requested(self, sleep_mock):
+ hook = SnowflakeSqlApiHook(snowflake_conn_id="test_conn")
+ hook.get_sql_api_query_status = mock.MagicMock(return_value={"status":
"error", "message": "oh no!"})
+
+ with pytest.raises(RuntimeError) as excinfo:
+ hook.wait_for_query("qid-789", raise_error=True)
+ assert str(excinfo.value) == "oh no!"
+ sleep_mock.assert_not_called()
+
+ @mock.patch(f"{MODULE_PATH}.time.sleep")
+ def test_wait_for_query_error_returns_when_not_raising(self, sleep_mock):
+ hook = SnowflakeSqlApiHook(snowflake_conn_id="test_conn")
+ hook.get_sql_api_query_status = mock.MagicMock(
+ return_value={"status": "error", "message": "still bad"}
+ )
+
+ result = hook.wait_for_query(query_id="qid-000", raise_error=False)
+ assert result == {"status": "error", "message": "still bad"}
+ sleep_mock.assert_not_called()
+
+ @mock.patch(f"{MODULE_PATH}.time.sleep")
+ def test_wait_for_query_handles_unknown_status(self, sleep_mock):
+ hook = SnowflakeSqlApiHook(snowflake_conn_id="test_conn")
+ hook.get_sql_api_query_status = mock.MagicMock(return_value={"status":
"queued", "info": ["a", "b"]})
+
+ result = hook.wait_for_query(query_id="qid-111")
+ assert result == {"status": "queued", "info": ["a", "b"]}
+ sleep_mock.assert_not_called()
+
+ @mock.patch(f"{MODULE_PATH}.time.time")
+ @mock.patch(f"{MODULE_PATH}.time.sleep")
+ def test_wait_for_query_timeout_error(self, sleep_mock, time_mock):
+ hook = SnowflakeSqlApiHook(snowflake_conn_id="test_conn")
+
+ # Simulate a query that keeps running and never finishes
+ hook.get_sql_api_query_status = mock.MagicMock(return_value={"status":
"running"})
+ time_mock.side_effect = list(range(5))
+
+ qid = "qid-789"
+ timeout = 3
+
+ with pytest.raises(TimeoutError):
+ hook.wait_for_query(query_id=qid, timeout=timeout, poll_interval=1)
+
+ # Ensure that we polled multiple times before the timeout error is
raised
+ assert sleep_mock.call_count == 3
+ sleep_mock.assert_has_calls([mock.call(1)] * 3)
+ assert hook.get_sql_api_query_status.call_count == 4
+
hook.get_sql_api_query_status.assert_has_calls([mock.call(query_id=qid)] * 4)
+ assert time_mock.call_count == 5
+
+ @mock.patch(f"{HOOK_PATH}._make_api_call_with_retries")
+ @mock.patch(f"{HOOK_PATH}._process_response")
+ @mock.patch(f"{HOOK_PATH}.get_request_url_header_params")
+ def test_get_result_from_successful_sql_api_query_no_data(
+ self, mock_get_url, mock_process_response, mock_api_call
+ ):
+ hook = SnowflakeSqlApiHook(snowflake_conn_id="test_conn")
+
+ mock_get_url.return_value = ("header", "params", "url")
+ mock_process_response.return_value = {"status": "success"}
+ mock_api_call.return_value = (200, {"data": []})
+
+ result =
hook.get_result_from_successful_sql_api_query(query_id="qid-1")
+
+ assert result == []
+ mock_get_url.assert_called_once_with("qid-1")
+ mock_api_call.assert_called_once_with("GET", "url", "header", "params")
+ mock_process_response.assert_called_once_with(200, {"data": []})
+
+ @mock.patch(f"{HOOK_PATH}._make_api_call_with_retries")
+ @mock.patch(f"{HOOK_PATH}._process_response")
+ @mock.patch(f"{HOOK_PATH}.get_request_url_header_params")
+ def test_get_result_from_successful_sql_api_query_no_column_metadata(
+ self, mock_get_url, mock_process_response, mock_api_call
+ ):
+ hook = SnowflakeSqlApiHook(snowflake_conn_id="test_conn")
+
+ mock_get_url.return_value = ("header", "params", "url")
+ mock_process_response.return_value = {"status": "success"}
+ mock_api_call.return_value = (200, {"data": [[1, 2]],
"resultSetMetaData": {"rowType": []}})
+
+ result =
hook.get_result_from_successful_sql_api_query(query_id="qid-2")
+
+ assert result == []
+ mock_get_url.assert_called_once_with("qid-2")
+ mock_api_call.assert_called_once_with("GET", "url", "header", "params")
+ mock_process_response.assert_called_once_with(
+ 200, {"data": [[1, 2]], "resultSetMetaData": {"rowType": []}}
+ )
+
+ @mock.patch(f"{HOOK_PATH}._make_api_call_with_retries")
+ @mock.patch(f"{HOOK_PATH}._process_response")
+ @mock.patch(f"{HOOK_PATH}.get_request_url_header_params")
+ def test_get_result_from_successful_sql_api_query_raises_error(
+ self, mock_get_url, mock_process_response, mock_api_call
+ ):
+ hook = SnowflakeSqlApiHook(snowflake_conn_id="test_conn")
+
+ mock_get_url.return_value = ("header", "params", "url")
+ mock_process_response.return_value = {"status": "failed"}
+ mock_api_call.return_value = (400, {"some": "response"})
+
+ with pytest.raises(
+ RuntimeError, match="Query must have status `success` to retrieve
data; got `failed`."
+ ):
+ hook.get_result_from_successful_sql_api_query(query_id="qid-3")
+
+ mock_get_url.assert_called_once_with("qid-3")
+ mock_api_call.assert_called_once_with("GET", "url", "header", "params")
+ mock_process_response.assert_called_once_with(400, {"some":
"response"})
+
+ @mock.patch(f"{HOOK_PATH}._make_api_call_with_retries")
+ @mock.patch(f"{HOOK_PATH}._process_response")
+ @mock.patch(f"{HOOK_PATH}.get_request_url_header_params")
+ def test_get_result_from_successful_sql_api_query_single_partition(
+ self, mock_get_url, mock_process_response, mock_api_call
+ ):
+ hook = SnowflakeSqlApiHook(snowflake_conn_id="test_conn")
+
+ mock_get_url.return_value = ("header", "params", "url")
+ mock_process_response.return_value = {"status": "success"}
+ mock_api_call.return_value = (
+ 200,
+ {
+ "data": [[1, "a"], [2, "b"]],
+ "resultSetMetaData": {
+ "rowType": [{"name": "id"}, {"name": "val"}],
+ "partitionInfo": [{"p0": "p0"}], # Single partition
+ },
+ },
+ )
+
+ result =
hook.get_result_from_successful_sql_api_query(query_id="qid-4")
+
+ expected = [{"id": 1, "val": "a"}, {"id": 2, "val": "b"}]
+ assert result == expected
+
+ # Only one API call (no additional partitions)
+ mock_api_call.assert_called_once_with("GET", "url", "header", "params")
+ mock_get_url.assert_called_once_with("qid-4")
+ mock_process_response.assert_called_once_with(
+ 200,
+ {
+ "data": [[1, "a"], [2, "b"]],
+ "resultSetMetaData": {
+ "rowType": [{"name": "id"}, {"name": "val"}],
+ "partitionInfo": [{"p0": "p0"}],
+ },
+ },
+ )
+
+ @mock.patch(f"{HOOK_PATH}._make_api_call_with_retries")
+ @mock.patch(f"{HOOK_PATH}._process_response")
+ @mock.patch(f"{HOOK_PATH}.get_request_url_header_params")
+ def test_get_result_from_successful_sql_api_query_multiple_partitions(
+ self, mock_get_url, mock_process_response, mock_api_call
+ ):
+ hook = SnowflakeSqlApiHook(snowflake_conn_id="test_conn")
+
+ # Initial response with metadata and 2 partitions
+ initial_response = {
+ "data": [[1, "a"], [2, "b"]],
+ "resultSetMetaData": {
+ "rowType": [{"name": "id"}, {"name": "val"}],
+ "partitionInfo": [{"p0": "p0"}, {"p1": "p1"}],
+ },
+ }
+ # Second partition response
+ second_response = {
+ "data": [[3, "c"], [4, "d"]],
+ "resultSetMetaData": {
+ "rowType": [{"name": "id"}, {"name": "val"}],
+ "partitionInfo": [{"p0": "p0"}, {"p1": "p1"}],
+ },
+ }
+
+ mock_get_url.return_value = ("header", "params",
"https://example.com/api/query")
+ mock_process_response.return_value = {"status": "success"}
+ mock_api_call.side_effect = [
+ (200, initial_response), # Initial call
+ (200, second_response), # Partition 1
+ ]
+
+ result =
hook.get_result_from_successful_sql_api_query(query_id="qid-5")
+
+ expected = [
+ {"id": 1, "val": "a"},
+ {"id": 2, "val": "b"},
+ {"id": 3, "val": "c"},
+ {"id": 4, "val": "d"},
+ ]
+ assert result == expected
+
+ # Two API calls: first for the initial query, second for partition 1
+ assert mock_api_call.call_count == 2
+ mock_api_call.assert_any_call("GET", "https://example.com/api/query",
"header", "params")
+ mock_api_call.assert_any_call("GET",
"https://example.com/api/query?partition=1", "header", "params")
+ mock_get_url.assert_called_once_with("qid-5")
+ mock_process_response.assert_called_once()
+
@pytest.mark.asyncio
async def test_make_api_call_with_retries_async_success(self,
mock_async_request):
"""