Skip to content

Commit 86d7c49

Browse files
committed
place holder
1 parent 04fa37b commit 86d7c49

File tree

7 files changed

+391
-22
lines changed

7 files changed

+391
-22
lines changed

src/snowflake/snowpark/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"QueryListener",
4141
"AsyncJob",
4242
"StoredProcedureProfiler",
43+
"UDFProfiler",
4344
]
4445

4546

@@ -54,6 +55,7 @@
5455
from snowflake.snowpark.async_job import AsyncJob
5556
from snowflake.snowpark.column import CaseExpr, Column
5657
from snowflake.snowpark.stored_procedure_profiler import StoredProcedureProfiler
58+
from snowflake.snowpark.udf_profiler import UDFProfiler
5759
from snowflake.snowpark.dataframe import DataFrame
5860
from snowflake.snowpark.dataframe_ai_functions import DataFrameAIFunctions
5961
from snowflake.snowpark.dataframe_analytics_functions import DataFrameAnalyticsFunctions

src/snowflake/snowpark/_internal/snowpark_profiler.py

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,13 @@
77
from typing import List, Literal, Optional
88

99
import snowflake.snowpark
10-
from snowflake.snowpark._internal.utils import (
11-
SNOWFLAKE_ANONYMOUS_CALL_WITH_PATTERN,
12-
)
1310

1411
logger = logging.getLogger(__name__)
1512

1613

1714
class SnowparkProfiler:
1815
"""
19-
Set up profiler to receive profiles of stored procedures. This feature cannot be used in owner's right stored
20-
procedure because owner's right stored procedure will not be able to set session-level parameters.
16+
Base class for stored procedure profiler and UDF profiler
2117
"""
2218

2319
def __init__(
@@ -31,20 +27,22 @@ def __init__(
3127
self._has_target_stage = False
3228
self._is_enabled = False
3329

34-
self._active_profiler_name = ""
30+
self._active_profiler_name = "ACTIVE_PYTHON_PROFILER"
3531
self._output_sql = ""
3632
self._profiler_module_name = ""
3733

38-
def register_modules(self, stored_procedures: Optional[List[str]] = None) -> None:
34+
def register_modules(self, modules: Optional[List[str]] = None) -> None:
3935
"""
40-
Register stored procedures to generate profiles for them.
36+
Register modules to generate profiles for them.
4137
4238
Args:
4339
stored_procedures: List of names of stored procedures. Registered modules will be overwritten by this input.
4440
Input None or an empty list will remove registered modules.
4541
"""
46-
sp_string = ",".join(stored_procedures) if stored_procedures is not None else ""
47-
sql_statement = f"alter session set {self._profiler_module_name}='{sp_string}'"
42+
module_string = ",".join(modules) if modules is not None else ""
43+
sql_statement = (
44+
f"alter session set {self._profiler_module_name}='{module_string}'"
45+
)
4846
self._session.sql(sql_statement)._internal_collect_with_tag_no_telemetry()
4947

5048
def set_active_profiler(
@@ -91,25 +89,24 @@ def disable(self) -> None:
9189
self._session.sql(sql_statement)._internal_collect_with_tag_no_telemetry()
9290

9391
@staticmethod
94-
def _is_sp_call(query: str) -> bool:
95-
query = query.upper().strip(" ")
96-
return SNOWFLAKE_ANONYMOUS_CALL_WITH_PATTERN.match(
97-
query
98-
) is not None or query.startswith("CALL")
92+
def _is_procedure_or_function_call(query: str) -> bool:
93+
pass
9994

10095
def _get_last_query_id(self) -> Optional[str]:
10196
current_thread = threading.get_ident()
10297
for query in self._query_history.queries[::-1]: # type: ignore
10398
query_thread = getattr(query, "thread_id", None)
104-
if query_thread == current_thread and self._is_sp_call(query.sql_text):
99+
if query_thread == current_thread and self._is_procedure_or_function_call(
100+
query.sql_text
101+
):
105102
return query.query_id
106103
return None
107104

108105
def get_output(self) -> str:
109106
"""
110-
Return the profiles of last executed stored procedure in current thread. If there is no previous
111-
stored procedure call, an error will be raised.
112-
Please call this function right after the stored procedure you want to profile to avoid any error.
107+
Return the profiles of last executed stored procedure or UDF in current thread. If there is no previous
108+
stored procedure or UDF call, an error will be raised.
109+
Please call this function right after the stored procedure or UDF you want to profile to avoid any error.
113110
114111
"""
115112
# return empty string when profiler is not enabled to not interrupt user's code
@@ -121,7 +118,7 @@ def get_output(self) -> str:
121118
query_id = self._get_last_query_id()
122119
if query_id is None:
123120
logger.warning(
124-
"You are seeing this warning because last executed stored procedure does not exist. Please run the store procedure before get profiler output."
121+
"You are seeing this warning because last executed stored procedure or UDF does not exist. Please run the store procedure or UDF before get profiler output."
125122
)
126123
return ""
127124
sql = self._output_sql.format(query_id=query_id)

src/snowflake/snowpark/session.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444
from snowflake.connector import ProgrammingError, SnowflakeConnection
4545
from snowflake.connector.options import installed_pandas, pandas, pyarrow
4646
from snowflake.connector.pandas_tools import write_pandas
47+
48+
from snowflake.snowpark import UDFProfiler
4749
from snowflake.snowpark._internal.analyzer import analyzer_utils
4850
from snowflake.snowpark._internal.analyzer.analyzer import Analyzer
4951
from snowflake.snowpark._internal.analyzer.analyzer_utils import (
@@ -807,6 +809,7 @@ def __init__(
807809
self._runtime_version_from_requirement: str = None
808810
self._temp_table_auto_cleaner: TempTableAutoCleaner = TempTableAutoCleaner(self)
809811
self._sp_profiler = StoredProcedureProfiler(session=self)
812+
self._udf_profiler = UDFProfiler(session=self)
810813
self._dataframe_profiler = DataframeProfiler(session=self)
811814
self._catalog = None
812815

@@ -4314,6 +4317,14 @@ def stored_procedure_profiler(self) -> StoredProcedureProfiler:
43144317
"""
43154318
return self._sp_profiler
43164319

4320+
@property
4321+
def udf_profiler(self) -> UDFProfiler:
4322+
"""
4323+
Returns a :class:`udf_profiler.UDFProfiler` object that you can use to profile UDFs.
4324+
See details of how to use this object in :class:`udf_profiler.UDFProfiler`.
4325+
"""
4326+
return self._udf_profiler
4327+
43174328
@property
43184329
def dataframe_profiler(self) -> DataframeProfiler:
43194330
"""

src/snowflake/snowpark/stored_procedure_profiler.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from snowflake.snowpark._internal.utils import (
1010
parse_table_name,
1111
strip_double_quotes_in_like_statement_in_table_name,
12+
SNOWFLAKE_ANONYMOUS_CALL_WITH_PATTERN,
1213
)
1314

1415
logger = logging.getLogger(__name__)
@@ -25,7 +26,6 @@ def __init__(
2526
session: "snowflake.snowpark.Session",
2627
) -> None:
2728
super().__init__(session)
28-
self._active_profiler_name = "ACTIVE_PYTHON_PROFILER"
2929
self._output_sql = (
3030
"select snowflake.core.get_python_profiler_output('{query_id}')"
3131
)
@@ -72,3 +72,10 @@ def set_active_profiler(
7272
"Target stage for profiler not found, using default stage of current session."
7373
)
7474
super().set_active_profiler(active_profiler_type)
75+
76+
@staticmethod
77+
def _is_procedure_or_function_call(query: str) -> bool:
78+
query = query.upper().strip(" ")
79+
return SNOWFLAKE_ANONYMOUS_CALL_WITH_PATTERN.match(
80+
query
81+
) is not None or query.startswith("CALL")
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#
2+
# Copyright (c) 2012-2025 Snowflake Computing Inc. All rights reserved.
3+
#
4+
5+
import logging
6+
import re
7+
8+
import snowflake.snowpark
9+
from snowflake.snowpark._internal.snowpark_profiler import SnowparkProfiler
10+
11+
12+
logger = logging.getLogger(__name__)
13+
14+
SNOWFLAKE_ANONYMOUS_FUNCTION_PATTERN = re.compile(
15+
r"^\s*WITH\s+\w+\s+AS\s+FUNCTION", re.IGNORECASE
16+
)
17+
18+
19+
class UDFProfiler(SnowparkProfiler):
20+
"""
21+
Set up profiler to receive profiles of UDFs.
22+
"""
23+
24+
def __init__(
25+
self,
26+
session: "snowflake.snowpark.Session",
27+
) -> None:
28+
super().__init__(session)
29+
30+
self._output_sql = "select * from table(SNOWFLAKE.LOCAL.GET_PYTHON_UDF_PROFILER_OUTPUT('{query_id}'));"
31+
self._profiler_module_name = "PYTHON_UDF_PROFILER_MODULES"
32+
33+
@staticmethod
34+
def _is_procedure_or_function_call(query: str) -> bool:
35+
query = query.upper().strip(" ")
36+
return SNOWFLAKE_ANONYMOUS_FUNCTION_PATTERN.match(
37+
query
38+
) is not None or query.startswith("SELECT")

tests/integ/test_stored_procedure_profiler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ def test_set_incorrect_active_profiler(
188188
def test_sp_call_match(profiler_session, sp_call_sql):
189189
pro = profiler_session.stored_procedure_profiler
190190

191-
assert pro._is_sp_call(sp_call_sql)
191+
assert pro._is_procedure_or_function_call(sp_call_sql)
192192

193193

194194
@pytest.mark.skipif(

0 commit comments

Comments
 (0)