Skip to content

Commit 307efa2

Browse files
SNOW-2011670 Allow url parameter requestId to be set with statement params (#2240)
As part of the Snowpark IR project, we will send AST as part of the request. To restore them faithfully on the server-side and avoid introducing yet another layer of indirection, we would like to use the HTTP Request Id. Currently, this Request Id is generated on the client as part of issuing the query. With this PR we introduce a new (client-side) statement parameter requestId that allows to overwrite the automatically generated request Id. requestId must be a valid UUID4 or UUID4 string. (cherry picked from commit 6e214d0)
1 parent 89e4888 commit 307efa2

File tree

4 files changed

+80
-2
lines changed

4 files changed

+80
-2
lines changed

DESCRIPTION.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Source code is also available at: https://github.com/snowflakedb/snowflake-conne
1919
- Fixed a bug that caused driver to fail silently on `TO_DATE` arrow to python conversion when invalid date was followed by the correct one.
2020
- Added `check_arrow_conversion_error_on_every_column` connection property that can be set to `False` to restore previous behaviour in which driver will ignore errors until it occurs in the last column. This flag's purpose is to unblock workflows that may be impacted by the bugfix and will be removed in later releases.
2121
- Lower log levels from info to debug for some of the messages to make the output easier to follow.
22+
- Allow the connector to inherit a UUID4 generated upstream, provided in statement parameters (field: `requestId`), rather than automatically generate a UUID4 to use for the HTTP Request ID.
2223

2324
- v3.14.0(March 03, 2025)
2425
- Bumped pyOpenSSL dependency upper boundary from <25.0.0 to <26.0.0.

src/snowflake/connector/_utils.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from enum import Enum
55
from random import choice
66
from threading import Timer
7+
from uuid import UUID
78

89

910
class TempObjectType(Enum):
@@ -29,6 +30,8 @@ class TempObjectType(Enum):
2930
"PYTHON_SNOWPARK_USE_SCOPED_TEMP_OBJECTS"
3031
)
3132

33+
REQUEST_ID_STATEMENT_PARAM_NAME = "requestId"
34+
3235

3336
def generate_random_alphanumeric(length: int = 10) -> str:
3437
return "".join(choice(ALPHANUMERIC) for _ in range(length))
@@ -42,6 +45,21 @@ def get_temp_type_for_object(use_scoped_temp_objects: bool) -> str:
4245
return SCOPED_TEMPORARY_STRING if use_scoped_temp_objects else TEMPORARY_STRING
4346

4447

48+
def is_uuid4(str_or_uuid: str | UUID) -> bool:
49+
"""Check whether provided string str is a valid UUID version4."""
50+
if isinstance(str_or_uuid, UUID):
51+
return str_or_uuid.version == 4
52+
53+
if not isinstance(str_or_uuid, str):
54+
return False
55+
56+
try:
57+
uuid_str = str(UUID(str_or_uuid, version=4))
58+
except ValueError:
59+
return False
60+
return uuid_str == str_or_uuid
61+
62+
4563
class _TrackedQueryCancellationTimer(Timer):
4664
def __init__(self, interval, function, args=None, kwargs=None):
4765
super().__init__(interval, function, args, kwargs)

src/snowflake/connector/cursor.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@
3535

3636
from . import compat
3737
from ._sql_util import get_file_transfer_type
38-
from ._utils import _TrackedQueryCancellationTimer
38+
from ._utils import (
39+
REQUEST_ID_STATEMENT_PARAM_NAME,
40+
_TrackedQueryCancellationTimer,
41+
is_uuid4,
42+
)
3943
from .bind_upload_agent import BindUploadAgent, BindUploadError
4044
from .constants import (
4145
CMD_TYPE_DOWNLOAD,
@@ -637,7 +641,27 @@ def _execute_helper(
637641
)
638642

639643
self._sequence_counter = self._connection._next_sequence_counter()
640-
self._request_id = uuid.uuid4()
644+
645+
# If requestId is contained in statement parameters, use it to set request id. Verify here it is a valid uuid4
646+
# identifier.
647+
if (
648+
statement_params is not None
649+
and REQUEST_ID_STATEMENT_PARAM_NAME in statement_params
650+
):
651+
request_id = statement_params[REQUEST_ID_STATEMENT_PARAM_NAME]
652+
653+
if not is_uuid4(request_id):
654+
# uuid.UUID will throw an error if invalid, but we explicitly check and throw here.
655+
raise ValueError(f"requestId {request_id} is not a valid UUID4.")
656+
self._request_id = uuid.UUID(str(request_id), version=4)
657+
658+
# Create a (deep copy) and remove the statement param, there is no need to encode it as extra parameter
659+
# one more time.
660+
statement_params = statement_params.copy()
661+
statement_params.pop(REQUEST_ID_STATEMENT_PARAM_NAME)
662+
else:
663+
# Generate UUID for query.
664+
self._request_id = uuid.uuid4()
641665

642666
logger.debug(f"Request id: {self._request_id}")
643667

test/integ/test_cursor.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import os
88
import pickle
99
import time
10+
import uuid
1011
from datetime import date, datetime, timezone
1112
from typing import TYPE_CHECKING, NamedTuple
1213
from unittest import mock
@@ -1964,3 +1965,37 @@ def test_nanoarrow_usage_deprecation():
19641965
and "snowflake.connector.cursor.NanoarrowUsage has been deprecated"
19651966
in str(record[2].message)
19661967
)
1968+
1969+
1970+
@pytest.mark.parametrize(
1971+
"request_id",
1972+
[
1973+
"THIS IS NOT VALID",
1974+
uuid.uuid1(),
1975+
uuid.uuid3(uuid.NAMESPACE_URL, "www.snowflake.com"),
1976+
uuid.uuid5(uuid.NAMESPACE_URL, "www.snowflake.com"),
1977+
],
1978+
)
1979+
def test_custom_request_id_negative(request_id, conn_cnx):
1980+
1981+
# Ensure that invalid request_ids (non uuid4) do not compromise interface.
1982+
with pytest.raises(ValueError, match="requestId"):
1983+
with conn_cnx() as con:
1984+
with con.cursor() as cur:
1985+
cur.execute(
1986+
"select seq4() as foo from table(generator(rowcount=>5))",
1987+
_statement_params={"requestId": request_id},
1988+
)
1989+
1990+
1991+
def test_custom_request_id(conn_cnx):
1992+
request_id = uuid.uuid4()
1993+
1994+
with conn_cnx() as con:
1995+
with con.cursor() as cur:
1996+
cur.execute(
1997+
"select seq4() as foo from table(generator(rowcount=>5))",
1998+
_statement_params={"requestId": request_id},
1999+
)
2000+
2001+
assert cur._sfqid is not None, "Query must execute successfully."

0 commit comments

Comments
 (0)