Skip to content

Commit adc273e

Browse files
sfc-gh-pbulawasfc-gh-fpawlowski
authored andcommitted
SNOW-1959514: Pandas single quote character fix (#2307)
(cherry picked from commit ecd5d9f)
1 parent 0c30584 commit adc273e

File tree

3 files changed

+53
-15
lines changed

3 files changed

+53
-15
lines changed

src/snowflake/connector/cursor.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -672,7 +672,10 @@ def _execute_helper(
672672
else:
673673
# or detect it.
674674
self._is_file_transfer = get_file_transfer_type(query) is not None
675-
logger.debug("is_file_transfer: %s", self._is_file_transfer is not None)
675+
logger.debug(
676+
"is_file_transfer: %s",
677+
self._is_file_transfer if self._is_file_transfer is not None else "None",
678+
)
676679

677680
real_timeout = (
678681
timeout if timeout and timeout > 0 else self._connection.network_timeout

src/snowflake/connector/pandas_tools.py

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -58,21 +58,26 @@ def build_location_helper(
5858
database: str | None, schema: str | None, name: str, quote_identifiers: bool
5959
) -> str:
6060
"""Helper to format table/stage/file format's location."""
61-
if quote_identifiers:
62-
location = (
63-
(('"' + database + '".') if database else "")
64-
+ (('"' + schema + '".') if schema else "")
65-
+ ('"' + name + '"')
66-
)
67-
else:
68-
location = (
69-
(database + "." if database else "")
70-
+ (schema + "." if schema else "")
71-
+ name
72-
)
61+
location = (
62+
(_escape_part_location(database, quote_identifiers) + "." if database else "")
63+
+ (_escape_part_location(schema, quote_identifiers) + "." if schema else "")
64+
+ _escape_part_location(name, quote_identifiers)
65+
)
7366
return location
7467

7568

69+
def _escape_part_location(part: str, should_quote: bool) -> str:
70+
if "'" in part:
71+
should_quote = True
72+
if should_quote:
73+
if not part.startswith('"'):
74+
part = '"' + part
75+
if not part.endswith('"'):
76+
part = part + '"'
77+
78+
return part
79+
80+
7681
def _do_create_temp_stage(
7782
cursor: SnowflakeCursor,
7883
stage_location: str,
@@ -473,6 +478,7 @@ def drop_object(name: str, object_type: str) -> None:
473478
drop_sql = f"DROP {object_type.upper()} IF EXISTS identifier(?) /* Python:snowflake.connector.pandas_tools.write_pandas() */"
474479
params = (name,)
475480
logger.debug(f"dropping {object_type} with '{drop_sql}'. params: %s", params)
481+
476482
cursor.execute(
477483
drop_sql,
478484
_is_internal=True,
@@ -570,10 +576,11 @@ def drop_object(name: str, object_type: str) -> None:
570576
num_statements=1,
571577
)
572578

579+
copy_stage_location = "@" + stage_location.replace("'", "\\'")
573580
copy_into_sql = (
574581
f"COPY INTO identifier(?) /* Python:snowflake.connector.pandas_tools.write_pandas() */ "
575582
f"({columns}) "
576-
f"FROM (SELECT {parquet_columns} FROM @{stage_location}) "
583+
f"FROM (SELECT {parquet_columns} FROM '{copy_stage_location}') "
577584
f"FILE_FORMAT=("
578585
f"TYPE=PARQUET "
579586
f"COMPRESSION={compression_map[compression]}"
@@ -582,7 +589,10 @@ def drop_object(name: str, object_type: str) -> None:
582589
f") "
583590
f"PURGE=TRUE ON_ERROR=?"
584591
)
585-
params = (target_table_location, on_error)
592+
params = (
593+
target_table_location,
594+
on_error,
595+
)
586596
logger.debug(f"copying into with '{copy_into_sql}'. params: %s", params)
587597
copy_results = cursor.execute(
588598
copy_into_sql,

test/integ/pandas/test_pandas_tools.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,8 @@ def mocked_execute(*args, **kwargs):
566566
(None, "schema", False, "schema"),
567567
(None, None, True, ""),
568568
(None, None, False, ""),
569+
("data'base", "schema", True, '"data\'base"."schema"'),
570+
("data'base", "schema", False, '"data\'base".schema'),
569571
],
570572
)
571573
def test_stage_location_building(
@@ -1101,3 +1103,26 @@ def test_write_pandas_with_on_error(
11011103
assert result["COUNT(*)"] == 1
11021104
finally:
11031105
cnx.execute_string(drop_sql)
1106+
1107+
1108+
def test_pandas_with_single_quote(
1109+
conn_cnx: Callable[..., Generator[SnowflakeConnection]],
1110+
):
1111+
random_table_name = random_string(5, "test'table")
1112+
table_name = f'"{random_table_name}"'
1113+
create_sql = f"CREATE OR REPLACE TABLE {table_name}(A INT)"
1114+
df_data = [[1]]
1115+
df = pandas.DataFrame(df_data, columns=["a"])
1116+
with conn_cnx() as cnx: # type: SnowflakeConnection
1117+
try:
1118+
cnx.execute_string(create_sql)
1119+
write_pandas(
1120+
cnx,
1121+
df,
1122+
table_name,
1123+
quote_identifiers=False,
1124+
auto_create_table=False,
1125+
index=False,
1126+
)
1127+
finally:
1128+
cnx.execute_string(f"drop table if exists {table_name}")

0 commit comments

Comments
 (0)