Skip to content

Commit 3e9305e

Browse files
SNOW-2303618: fix DataFrame.limit() fail if there is parameter binding in the executed SQL. (#3780)
1 parent 5ec047f commit 3e9305e

6 files changed

Lines changed: 54 additions & 18 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
### Snowpark Python API Updates
66

7+
#### Bug Fixes
8+
9+
- Fixed a bug that `DataFrame.limit()` fail if there is parameter binding in the executed SQL.
10+
711
#### New Features
812

913
- Added a new module `snowflake.snowpark.secrets` that provides Python wrappers for accessing Snowflake Secrets within Python UDFs and stored procedures that execute inside Snowflake.

src/snowflake/snowpark/_internal/analyzer/schema_utils.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# Copyright (c) 2012-2025 Snowflake Computing Inc. All rights reserved.
33
#
44
import traceback
5-
from typing import TYPE_CHECKING, List, Union, Optional
5+
from typing import TYPE_CHECKING, List, Union, Optional, Sequence, Any
66

77
import snowflake.snowpark
88
from snowflake.connector.cursor import ResultMetadata, SnowflakeCursor
@@ -70,6 +70,7 @@ def analyze_attributes(
7070
sql: str,
7171
session: "snowflake.snowpark.session.Session",
7272
dataframe_uuid: Optional[str] = None,
73+
query_params: Optional[Sequence[Any]] = None,
7374
) -> List[Attribute]:
7475
lowercase = sql.strip().lower()
7576

@@ -104,7 +105,7 @@ def analyze_attributes(
104105
stack = traceback.extract_stack(limit=10)[:-1]
105106
stack_trace = [frame.line for frame in stack] if len(stack) > 0 else None
106107
with measure_time() as e2e_time:
107-
attributes = session._get_result_attributes(sql)
108+
attributes = session._get_result_attributes(sql, query_params)
108109
session._conn._telemetry_client.send_describe_query_details(
109110
session._session_id, sql, e2e_time(), stack_trace
110111
)
@@ -118,9 +119,9 @@ def analyze_attributes(
118119

119120
@ttl_cache(ttl_seconds=15)
120121
def cached_analyze_attributes(
121-
sql: str, session: "snowflake.snowpark.session.Session", dataframe_uuid: Optional[str] = None # type: ignore
122+
sql: str, session: "snowflake.snowpark.session.Session", dataframe_uuid: Optional[str] = None, query_params: Optional[Sequence[Any]] = None # type: ignore
122123
) -> List[Attribute]:
123-
return analyze_attributes(sql, session, dataframe_uuid)
124+
return analyze_attributes(sql, session, dataframe_uuid, query_params)
124125

125126

126127
def convert_result_meta_to_attribute(
@@ -162,7 +163,7 @@ def get_new_description(
162163

163164

164165
def run_new_describe(
165-
cursor: SnowflakeCursor, query: str
166+
cursor: SnowflakeCursor, query: str, query_params: Optional[Sequence[Any]] = None
166167
) -> Union[List[ResultMetadata], List["ResultMetadataV2"]]: # pyright: ignore
167168
"""Execute describe() on a cursor, returning the new metadata format if possible.
168169
@@ -172,8 +173,5 @@ def run_new_describe(
172173
# ResultMetadataV2 may not currently be a type, depending on the connector
173174
# version, so the argument types are pyright ignored
174175

175-
if hasattr(cursor, "_describe_internal"):
176-
# Pyright does not perform narrowing here
177-
return cursor._describe_internal(query) # pyright: ignore
178-
else:
179-
return cursor.describe(query)
176+
# Pyright does not perform narrowing here
177+
return cursor._describe_internal(query, params=query_params) # pyright: ignore

src/snowflake/snowpark/_internal/analyzer/snowflake_plan.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -588,10 +588,15 @@ def _analyze_attributes(self) -> List[Attribute]:
588588
assert (
589589
self.schema_query is not None
590590
), "No schema query is available for the SnowflakePlan"
591+
query_params = getattr(self.source_plan, "query_params", None)
591592
if self.session.reduce_describe_query_enabled:
592-
return cached_analyze_attributes(self.schema_query, self.session, self.uuid)
593+
return cached_analyze_attributes(
594+
self.schema_query, self.session, self.uuid, query_params
595+
)
593596
else:
594-
return analyze_attributes(self.schema_query, self.session, self.uuid)
597+
return analyze_attributes(
598+
self.schema_query, self.session, self.uuid, query_params
599+
)
595600

596601
@property
597602
def attributes(self) -> List[Attribute]:

src/snowflake/snowpark/_internal/server_connection.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -269,15 +269,22 @@ def _get_string_datum(self, query: str) -> Optional[str]:
269269
rows = result_set_to_rows(self.run_query(query)["data"])
270270
return rows[0][0] if len(rows) > 0 else None
271271

272-
def get_result_attributes(self, query: str) -> List[Attribute]:
272+
def get_result_attributes(
273+
self, query: str, query_params: Optional[Sequence[Any]] = None
274+
) -> List[Attribute]:
273275
return convert_result_meta_to_attribute(
274-
self._run_new_describe(self._cursor, query), self.max_string_size
276+
self._run_new_describe(self._cursor, query, query_params=query_params),
277+
self.max_string_size,
275278
)
276279

277280
def _run_new_describe(
278-
self, cursor: SnowflakeCursor, query: str, **kwargs: dict
281+
self,
282+
cursor: SnowflakeCursor,
283+
query: str,
284+
query_params: Optional[Sequence[Any]] = None,
285+
**kwargs: dict,
279286
) -> Union[List[ResultMetadata], List["ResultMetadataV2"]]:
280-
result_metadata = run_new_describe(cursor, query)
287+
result_metadata = run_new_describe(cursor, query, query_params)
281288

282289
with self._lock:
283290
for listener in filter(

src/snowflake/snowpark/session.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3029,8 +3029,10 @@ def _run_query(
30293029
_statement_params=statement_params,
30303030
)["data"]
30313031

3032-
def _get_result_attributes(self, query: str) -> List[Attribute]:
3033-
return self._conn.get_result_attributes(query)
3032+
def _get_result_attributes(
3033+
self, query: str, query_params: Optional[Sequence[Any]] = None
3034+
) -> List[Attribute]:
3035+
return self._conn.get_result_attributes(query, query_params)
30343036

30353037
def get_session_stage(
30363038
self,

tests/integ/test_dataframe.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4690,6 +4690,26 @@ def test_limit_offset(session):
46904690
assert df.limit(1, offset=1).collect() == [Row(A=4, B=5, C=6)]
46914691

46924692

4693+
@pytest.mark.skipif(
4694+
"config.getoption('local_testing_mode', default=False)",
4695+
reason="Not supported in local testing ",
4696+
)
4697+
def test_limit_param_binding(session):
4698+
table_name = Utils.random_name_for_temp_object(TempObjectType.TABLE)
4699+
session.create_dataframe(
4700+
[[{"name": "Alice"}]], schema=StructType([StructField("col", VariantType())])
4701+
).write.save_as_table(table_name, table_type="temp")
4702+
result = session.sql(
4703+
f"""
4704+
SELECT col:name as Name
4705+
FROM {table_name}
4706+
WHERE GET_PATH(col, cast(? as VARCHAR)) IS NOT NULL
4707+
""",
4708+
["name"],
4709+
).limit(1)
4710+
Utils.check_answer(result, [Row(NAME='"Alice"')])
4711+
4712+
46934713
def test_df_join_how_on_overwrite(session):
46944714
df1 = session.create_dataframe([[1, 1, "1"], [2, 2, "3"]]).to_df(
46954715
["int", "int2", "str"]

0 commit comments

Comments
 (0)