Skip to content

Commit 9fc9b76

Browse files
sfc-gh-bchinnsfc-gh-turbaszek
authored andcommitted
Fix get_results_from_sfqid with DictCursor + multi statements (#2531)
Port PR: snowflakedb/Stored-Proc-Python-Connector#225 I noticed that get_results_from_sfqid assumes that fetchall returns a tuple when that's not necessarily the case. Fixing it here + adding a test that fails without the fix. ``` E AssertionError: assert [{'multiple s...ccessfully.'}] == [{'1': 1}] E At index 0 diff: {'multiple statement execution': 'Multiple statements executed successfully.'} != {'1': 1} E Full diff: E - [{'1': 1}] E + [{'multiple statement execution': 'Multiple statements executed successfully.'}] ```
1 parent 040682d commit 9fc9b76

File tree

7 files changed

+51
-44
lines changed

7 files changed

+51
-44
lines changed

DESCRIPTION.md

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,36 +7,9 @@ https://docs.snowflake.com/
77
Source code is also available at: https://github.com/snowflakedb/snowflake-connector-python
88

99
# Release Notes
10-
- v4.1.0(TBD)
11-
- Added the `SNOWFLAKE_AUTH_FORCE_SERVER` environment variable to force the use of the local-listening server when using the `externalbrowser` auth method.
12-
- This allows headless environments (like Docker or Airflow) running locally to auth via a browser URL.
13-
- Fix compilation error when building from sources with libc++.
14-
15-
- v4.0.0(October 09,2025)
16-
- Added support for checking certificates revocation using revocation lists (CRLs)
17-
- Added `CERT_REVOCATION_CHECK_MODE` to `CLIENT_ENVIRONMENT`
10+
- v3.18.0(TBD)
1811
- Added the `workload_identity_impersonation_path` parameter to support service account impersonation for Workload Identity Federation on GCP and AWS workloads only
1912
- Fixed `get_results_from_sfqid` when using `DictCursor` and executing multiple statements at once
20-
- Added the `oauth_credentials_in_body` parameter supporting an option to send the oauth client credentials in the request body
21-
- Fix retry behavior for `ECONNRESET` error
22-
- Added an option to exclude `botocore` and `boto3` dependencies by setting `SNOWFLAKE_NO_BOTO` environment variable during installation
23-
- Revert changing exception type in case of token expired scenario for `Oauth` authenticator back to `DatabaseError`
24-
- Enhanced configuration file security checks with stricter permission validation.
25-
- Configuration files writable by group or others now raise a `ConfigSourceError` with detailed permission information, preventing potential credential tampering.
26-
- Fixed the return type of `SnowflakeConnection.cursor(cursor_class)` to match the type of `cursor_class`
27-
- Constrained the types of `fetchone`, `fetchmany`, `fetchall`
28-
- As part of this fix, `DictCursor` is no longer a subclass of `SnowflakeCursor`; use `SnowflakeCursorBase` as a superclass of both.
29-
- Fix "No AWS region was found" error if AWS region was set in `AWS_DEFAULT_REGION` variable instead of `AWS_REGION` for `WORKLOAD_IDENTITY` authenticator
30-
- Add `ocsp_root_certs_dict_lock_timeout` connection parameter to set the timeout (in seconds) for acquiring the lock on the OCSP root certs dictionary. Default value for this parameter is -1 which indicates no timeout.
31-
- Fixed behaviour of trying S3 Transfer Accelerate endpoint by default for internal stages, and always getting HTTP403 due to permissions missing on purpose. Now /accelerate is not attempted.
32-
33-
- v3.18.0(October 03,2025)
34-
- Added support for pandas conversion for Day-time and Year-Month Interval types
35-
36-
- v3.17.4(September 22,2025)
37-
- Added support for intermediate certificates as roots when they are stored in the trust store
38-
- Bumped up vendored `urllib3` to `2.5.0` and `requests` to `v2.32.5`
39-
- Dropped support for OpenSSL versions older than 1.1.1
4013

4114
- v3.17.3(September 02,2025)
4215
- Enhanced configuration file permission warning messages.

src/snowflake/connector/aio/_cursor.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1279,8 +1279,7 @@ async def wait_until_ready() -> None:
12791279
await self.connection.get_query_status_throw_if_error(
12801280
sfqid
12811281
) # Trigger an exception if query failed
1282-
klass = self.__class__
1283-
self._inner_cursor = klass(self.connection)
1282+
self._inner_cursor = SnowflakeCursor(self.connection)
12841283
self._sfqid = sfqid
12851284
self._prefetch_hook = wait_until_ready
12861285

src/snowflake/connector/cursor.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1749,8 +1749,7 @@ def wait_until_ready() -> None:
17491749
self.connection.get_query_status_throw_if_error(
17501750
sfqid
17511751
) # Trigger an exception if query failed
1752-
klass = self.__class__
1753-
self._inner_cursor = klass(self.connection)
1752+
self._inner_cursor = SnowflakeCursor(self.connection)
17541753
self._sfqid = sfqid
17551754
self._prefetch_hook = wait_until_ready
17561755

test/integ/aio_it/test_async_async.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,22 @@
1111
import pytest
1212

1313
from snowflake.connector import DatabaseError, ProgrammingError
14+
from snowflake.connector.aio import DictCursor, SnowflakeCursor
1415
from snowflake.connector.constants import QueryStatus
1516

1617
# Mark all tests in this file to time out after 2 minutes to prevent hanging forever
1718
pytestmark = pytest.mark.timeout(120)
1819

1920

20-
async def test_simple_async(conn_cnx):
21+
@pytest.mark.parametrize("cursor_class", [SnowflakeCursor, DictCursor])
22+
async def test_simple_async(conn_cnx, cursor_class):
2123
"""Simple test to that shows the most simple usage of fire and forget.
2224
2325
This test also makes sure that wait_until_ready function's sleeping is tested and
2426
that some fields are copied over correctly from the original query.
2527
"""
2628
async with conn_cnx() as con:
27-
async with con.cursor() as cur:
29+
async with con.cursor(cursor_class) as cur:
2830
await cur.execute_async(
2931
"select count(*) from table(generator(timeLimit => 5))"
3032
)

test/integ/aio_it/test_multi_statement_async.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import pytest
1111

1212
from snowflake.connector import ProgrammingError, errors
13-
from snowflake.connector.aio import SnowflakeCursor
13+
from snowflake.connector.aio import DictCursor, SnowflakeCursor
1414
from snowflake.connector.constants import PARAMETER_MULTI_STATEMENT_COUNT, QueryStatus
1515
from snowflake.connector.util_text import random_string
1616

@@ -141,10 +141,11 @@ async def test_binding_multi(conn_cnx, style: str, skip_to_last_set: bool):
141141
)
142142

143143

144-
async def test_async_exec_multi(conn_cnx, skip_to_last_set: bool):
144+
@pytest.mark.parametrize("cursor_class", [SnowflakeCursor, DictCursor])
145+
async def test_async_exec_multi(conn_cnx, cursor_class, skip_to_last_set: bool):
145146
"""Tests whether async execution query works within a multi-statement"""
146147
async with conn_cnx() as con:
147-
async with con.cursor() as cur:
148+
async with con.cursor(cursor_class) as cur:
148149
await cur.execute_async(
149150
"select 1; select 2; select count(*) from table(generator(timeLimit => 1)); select 'b';",
150151
num_statements=4,
@@ -162,9 +163,23 @@ async def test_async_exec_multi(conn_cnx, skip_to_last_set: bool):
162163
)
163164

164165
await cur.get_results_from_sfqid(q_id)
166+
if cursor_class == SnowflakeCursor:
167+
expected = [
168+
[(1,)],
169+
[(2,)],
170+
lambda x: len(x) == 1 and len(x[0]) == 1 and x[0][0] > 0,
171+
[("b",)],
172+
]
173+
elif cursor_class == DictCursor:
174+
expected = [
175+
[{"1": 1}],
176+
[{"2": 2}],
177+
lambda x: len(x) == 1 and len(x[0]) == 1 and x[0]["COUNT(*)"] > 0,
178+
[{"'B'": "b"}],
179+
]
165180
await _check_multi_statement_results(
166181
cur,
167-
checks=[[(1,)], [(2,)], lambda x: x > [(0,)], [("b",)]],
182+
checks=expected,
168183
skip_to_last_set=skip_to_last_set,
169184
)
170185

test/integ/test_async.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import pytest
88

99
from snowflake.connector import DatabaseError, ProgrammingError
10+
from snowflake.connector.cursor import DictCursor, SnowflakeCursor
1011

1112
# Mark all tests in this file to time out after 2 minutes to prevent hanging forever
1213
pytestmark = [pytest.mark.timeout(120), pytest.mark.skipolddriver]
@@ -17,14 +18,15 @@
1718
QueryStatus = None
1819

1920

20-
def test_simple_async(conn_cnx):
21+
@pytest.mark.parametrize("cursor_class", [SnowflakeCursor, DictCursor])
22+
def test_simple_async(conn_cnx, cursor_class):
2123
"""Simple test to that shows the most simple usage of fire and forget.
2224
2325
This test also makes sure that wait_until_ready function's sleeping is tested and
2426
that some fields are copied over correctly from the original query.
2527
"""
2628
with conn_cnx() as con:
27-
with con.cursor() as cur:
29+
with con.cursor(cursor_class) as cur:
2830
cur.execute_async("select count(*) from table(generator(timeLimit => 5))")
2931
cur.get_results_from_sfqid(cur.sfqid)
3032
assert len(cur.fetchall()) == 1

test/integ/test_multi_statement.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import snowflake.connector.cursor
1616
from snowflake.connector import ProgrammingError, errors
17+
from snowflake.connector.cursor import DictCursor, SnowflakeCursor
1718

1819
try: # pragma: no cover
1920
from snowflake.connector.constants import (
@@ -153,10 +154,11 @@ def test_binding_multi(conn_cnx, style: str, skip_to_last_set: bool):
153154
)
154155

155156

156-
def test_async_exec_multi(conn_cnx, skip_to_last_set: bool):
157+
@pytest.mark.parametrize("cursor_class", [SnowflakeCursor, DictCursor])
158+
def test_async_exec_multi(conn_cnx, cursor_class, skip_to_last_set: bool):
157159
"""Tests whether async execution query works within a multi-statement"""
158160
with conn_cnx() as con:
159-
with con.cursor() as cur:
161+
with con.cursor(cursor_class) as cur:
160162
cur.execute_async(
161163
"select 1; select 2; select count(*) from table(generator(timeLimit => 1)); select 'b';",
162164
num_statements=4,
@@ -165,14 +167,29 @@ def test_async_exec_multi(conn_cnx, skip_to_last_set: bool):
165167
assert con.is_still_running(con.get_query_status(q_id))
166168
_wait_while_query_running(con, q_id, sleep_time=1)
167169
with conn_cnx() as con:
168-
with con.cursor() as cur:
170+
with con.cursor(cursor_class) as cur:
169171
_wait_until_query_success(con, q_id, num_checks=3, sleep_per_check=1)
170172
assert con.get_query_status_throw_if_error(q_id) == QueryStatus.SUCCESS
171173

174+
if cursor_class == SnowflakeCursor:
175+
expected = [
176+
[(1,)],
177+
[(2,)],
178+
lambda x: len(x) == 1 and len(x[0]) == 1 and x[0][0] > 0,
179+
[("b",)],
180+
]
181+
elif cursor_class == DictCursor:
182+
expected = [
183+
[{"1": 1}],
184+
[{"2": 2}],
185+
lambda x: len(x) == 1 and len(x[0]) == 1 and x[0]["COUNT(*)"] > 0,
186+
[{"'B'": "b"}],
187+
]
188+
172189
cur.get_results_from_sfqid(q_id)
173190
_check_multi_statement_results(
174191
cur,
175-
checks=[[(1,)], [(2,)], lambda x: x > [(0,)], [("b",)]],
192+
checks=expected,
176193
skip_to_last_set=skip_to_last_set,
177194
)
178195

0 commit comments

Comments
 (0)