From 86a25244440aeb0fcb23c6ea6c91842df3def859 Mon Sep 17 00:00:00 2001 From: Jonathan Shi Date: Tue, 21 Apr 2026 17:08:52 -0700 Subject: [PATCH 1/3] propagate parameters through describe queries --- .../_internal/analyzer/select_statement.py | 9 +- tests/integ/test_bind_variable.py | 82 ++++++++++++++++++- 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/src/snowflake/snowpark/_internal/analyzer/select_statement.py b/src/snowflake/snowpark/_internal/analyzer/select_statement.py index e59afd4034..7234c63b71 100644 --- a/src/snowflake/snowpark/_internal/analyzer/select_statement.py +++ b/src/snowflake/snowpark/_internal/analyzer/select_statement.py @@ -672,9 +672,12 @@ def children_plan_nodes(self) -> List[Union["Selectable", SnowflakePlan]]: @SnowflakePlan.Decorator.wrap_exception def _analyze_attributes( - sql: str, session: "snowflake.snowpark.session.Session", dataframe_uuid: Optional[str] = None # type: ignore + sql: str, + session: "snowflake.snowpark.session.Session", + dataframe_uuid: Optional[str] = None, # type: ignore + query_params: Optional[Sequence[Any]] = None, ) -> List[Attribute]: - return analyze_attributes(sql, session, dataframe_uuid) + return analyze_attributes(sql, session, dataframe_uuid, query_params) class SelectSQL(Selectable): @@ -707,7 +710,7 @@ def __init__( self.pre_actions[0].query_id_place_holder ) self._schema_query = analyzer_utils.schema_value_statement( - _analyze_attributes(sql, self._session, self._uuid) + _analyze_attributes(sql, self._session, self._uuid, query_params=params) ) # Change to subqueryable schema query so downstream query plan can describe the SQL self._query_param = None else: diff --git a/tests/integ/test_bind_variable.py b/tests/integ/test_bind_variable.py index 884439949a..306bf0802d 100644 --- a/tests/integ/test_bind_variable.py +++ b/tests/integ/test_bind_variable.py @@ -9,7 +9,7 @@ import pytest from snowflake.snowpark import Row -from snowflake.snowpark._internal.utils import is_in_stored_procedure +from snowflake.snowpark._internal.utils import TempObjectType, is_in_stored_procedure from snowflake.snowpark.exceptions import SnowparkSQLException from snowflake.snowpark.functions import col, lit, max, table_function from snowflake.snowpark.types import ( @@ -457,3 +457,83 @@ def test_explain(session): params=[1, "a", 2, "b"], ) df.explain() + + +@pytest.fixture(scope="module") +def proc_name(session): + """Create a trivial stored procedure that echoes its inputs back.""" + name = f"{session.get_fully_qualified_current_schema()}.{Utils.random_name_for_temp_object(TempObjectType.PROCEDURE)}" + session.sql( + f""" + CREATE OR REPLACE TEMPORARY PROCEDURE {name}(template VARCHAR, args VARCHAR) + RETURNS VARCHAR + LANGUAGE SQL + AS + $$ + BEGIN + RETURN template || ' | ' || args; + END; + $$ + """ + ).collect() + return name + + +class TestCallIdentifierBinding: + """ + SNOW-3061745: Bindings in CALL previously were not properly transferred through the expression tree. + These previously errored out when a chained operation after `session.sql` triggered a call to + `to_subqueryable`, which did not properly populate binding parameters. + """ + + def test_call_collect(self, session, proc_name): + result = session.sql( + "CALL identifier(?)(?, to_varchar(parse_json(?)))", + params=[proc_name, "tmpl", '{"a": 1}'], + ).collect() + assert result == [Row('tmpl | {"a":1}')] + + def test_call_select(self, session, proc_name): + result = ( + session.sql( + "CALL identifier(?)(?, ?)", + params=[proc_name, "tmpl", "args"], + ) + .select("*") + .collect() + ) + assert result == [Row("tmpl | args")] + + def test_call_filter(self, session, proc_name): + result = ( + session.sql( + "CALL identifier(?)(?, ?)", + params=[proc_name, "tmpl", "args"], + ) + .filter("1=1") + .collect() + ) + assert result == [Row("tmpl | args")] + + def test_call_sort(self, session, proc_name): + result = ( + session.sql( + "CALL identifier(?)(?, ?)", + params=[proc_name, "tmpl", "args"], + ) + .sort("$1") + .collect() + ) + assert result == [Row("tmpl | args")] + + def test_call_union(self, session, proc_name): + df1 = session.sql( + "CALL identifier(?)(?, ?)", + params=[proc_name, "tmpl1", "args1"], + ) + df2 = session.sql( + "CALL identifier(?)(?, ?)", + params=[proc_name, "tmpl2", "args2"], + ) + result = df1.union_all(df2).collect() + assert result == [Row("tmpl1 | args1"), Row("tmpl2 | args2")] From 6034f66c6c797189db534b981edfb07b2535d523 Mon Sep 17 00:00:00 2001 From: Jonathan Shi Date: Tue, 28 Apr 2026 10:51:10 -0700 Subject: [PATCH 2/3] changelog --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 959915450f..fd5e19df93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Release History +## 1.51.0 (TBD) + +### Snowpark Python API Updates + +#### New Features + +#### Bug Fixes + +- Fixed a bug where using parameter bindings for `CALL` queries issued through `session.sql` would raise an error. + ## 1.50.0 (2026-04-23) ### Snowpark Python API Updates From f71952d84d6dd4c6b688e43f6f31bf557caf6bc6 Mon Sep 17 00:00:00 2001 From: Jonathan Shi Date: Tue, 28 Apr 2026 11:02:44 -0700 Subject: [PATCH 3/3] move type: ignore annotation --- src/snowflake/snowpark/_internal/analyzer/select_statement.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/snowflake/snowpark/_internal/analyzer/select_statement.py b/src/snowflake/snowpark/_internal/analyzer/select_statement.py index 7234c63b71..028dfdec72 100644 --- a/src/snowflake/snowpark/_internal/analyzer/select_statement.py +++ b/src/snowflake/snowpark/_internal/analyzer/select_statement.py @@ -673,8 +673,8 @@ def children_plan_nodes(self) -> List[Union["Selectable", SnowflakePlan]]: @SnowflakePlan.Decorator.wrap_exception def _analyze_attributes( sql: str, - session: "snowflake.snowpark.session.Session", - dataframe_uuid: Optional[str] = None, # type: ignore + session: "snowflake.snowpark.session.Session", # type: ignore + dataframe_uuid: Optional[str] = None, query_params: Optional[Sequence[Any]] = None, ) -> List[Attribute]: return analyze_attributes(sql, session, dataframe_uuid, query_params)