Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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", # type: ignore
dataframe_uuid: Optional[str] = None,
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):
Expand Down Expand Up @@ -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:
Expand Down
82 changes: 81 additions & 1 deletion tests/integ/test_bind_variable.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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")]
Loading