From 75ede791588cc9f8d9a7890114071ee15ece624d Mon Sep 17 00:00:00 2001 From: Adam Ling Date: Tue, 30 Jun 2026 20:59:38 +0000 Subject: [PATCH 1/4] SNOW-3718333: Escape backslashes and single quotes in stage/file path SQL generation Stage and file paths passed to COPY INTO / PUT / GET were escaped for single quotes but not backslashes, so a path containing a backslash followed by a single quote produced invalid SQL. normalize_path now escapes backslashes before single quotes so the path stays a single string literal. Adds unit tests and integ tests covering Snowpark write.csv and Snowpark-pandas to_csv with quote/backslash paths. --- CHANGELOG.md | 1 + src/snowflake/snowpark/_internal/utils.py | 8 ++- tests/integ/modin/io/test_to_csv.py | 58 +++++++++++++++- .../scala/test_dataframe_writer_suite.py | 49 ++++++++++++++ tests/unit/scala/test_utils_suite.py | 4 +- tests/unit/test_internal_utils.py | 66 +++++++++++++++++++ 6 files changed, 183 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b00d66cbb..8a84e18694 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ #### Bug Fixes - Fixed a bug where stage paths and file format names that contain single quotes were not consistently escaped when generating SQL, which could produce malformed statements. This affects `INFER_SCHEMA` (used by `DataFrameReader.csv`/`json`/`parquet`/`orc`/`avro`) and `COPY FILES` (used by `FileOperation.copy_files`). +- Fixed a bug where single quotes and backslashes in stage/file paths were not correctly escaped when generating `COPY INTO` / `PUT` / `GET` SQL, which could produce malformed statements. This affects `DataFrame.write.csv`/`copy_into_location` and the Snowpark-pandas `DataFrame.to_csv` stage path. - Fixed a bug where UDF default argument values reconstructed from a source file in `register_from_file` were evaluated with `eval()`; they are now evaluated only against the documented set of supported default-value types, and unsupported expressions are ignored. - Fixed a bug where `object_name`, `object_domain`, or `object_version` values containing single quotes or backslashes in `session.lineage.trace()` caused incorrect SQL generation. These values are now properly escaped before being embedded in the `SYSTEM$DGQL` call. diff --git a/src/snowflake/snowpark/_internal/utils.py b/src/snowflake/snowpark/_internal/utils.py index 7927f5d37f..d2d258d759 100644 --- a/src/snowflake/snowpark/_internal/utils.py +++ b/src/snowflake/snowpark/_internal/utils.py @@ -429,7 +429,13 @@ def normalize_path(path: str, is_local: bool) -> str: return path if is_local and OPERATING_SYSTEM == "Windows": path = path.replace("\\", "/") - path = path.strip().replace("'", "\\'") + # Escape characters that are special inside a Snowflake single-quoted string + # literal. Backslash must be escaped before the single quote: otherwise a + # path containing ``\'`` would be written as ``\\'``, which Snowflake decodes + # as ``\`` followed by an unescaped quote that closes the literal early and + # produces invalid SQL. Escaping the backslash first keeps the path a single + # literal value. + path = path.strip().replace("\\", "\\\\").replace("'", "\\'") if not any(path.startswith(prefix) for prefix in prefixes): path = f"{prefixes[0]}{path}" return f"'{path}'" diff --git a/tests/integ/modin/io/test_to_csv.py b/tests/integ/modin/io/test_to_csv.py index e2027445cb..cdfab5735c 100644 --- a/tests/integ/modin/io/test_to_csv.py +++ b/tests/integ/modin/io/test_to_csv.py @@ -13,7 +13,7 @@ from numpy.testing import assert_equal import snowflake.snowpark.modin.plugin # noqa: F401 -from tests.integ.utils.sql_counter import sql_count_checker +from tests.integ.utils.sql_counter import sql_count_checker, SqlCounter from tests.utils import Utils temp_dir = tempfile.TemporaryDirectory() @@ -293,3 +293,59 @@ def test_timedeltaindex_to_csv_dataframe_local(): pd.DataFrame(native_df).to_csv(snow_path) assert_file_equal(snow_path, native_path, is_compressed=False) + + +def test_to_csv_stage_path_escapes_special_characters(sf_stage, session): + """Snowpark-pandas ``to_csv`` to a stage path must escape special characters + in the path. + + ``DataFrame.to_csv(path_or_buf="@stage/...")`` routes server-side into + ``snowpark_df.write.csv(location=...)`` -> ``normalize_path`` -> + ``COPY INTO ``. A path containing a backslash immediately followed + by a single quote must stay inside the stage-location string literal so the + generated ``COPY INTO`` is valid and the path is treated as literal data. + """ + snow_df = pd.DataFrame({"A": ["one", "two", "three"], "B": [1, 2, 3]}) + # None index name is not supported when writing to a Snowflake stage. + snow_df.index.set_names(["X"], inplace=True) + + # (a) Stage path whose directory name contains a single quote and a + # backslash. The write must succeed and the bytes must land verbatim on + # the stage (the path is data, not SQL). ``to_csv`` to a stage emits one + # query (the COPY INTO). + legit_name = "o'clock\\dir/mods.csv" + legit_path = f"@{sf_stage}/{legit_name}" + with SqlCounter(query_count=1): + snow_df.to_csv(legit_path, index=False) + listed = [row[0] for row in session.sql(f"LIST '@{sf_stage}'").collect()] + assert any(name.endswith("o'clock\\dir/mods.csv") for name in listed), listed + + # (b) A file name containing several special characters at once: a backslash + # followed by a single quote, parentheses, a comma and a trailing ``--``. + # These must all be treated as literal characters of the path. + special_name = "report\\' , (note) -- draft" + special_path = f"@{sf_stage}/{special_name}" + with SqlCounter(query_count=1): + snow_df.to_csv(special_path, index=False) + + # The whole name survives as a single physical file. + listed = [row[0] for row in session.sql(f"LIST '@{sf_stage}'").collect()] + assert any(special_name in name for name in listed), listed + + # Download the file and verify its contents are exactly the DataFrame's own + # rows -- confirming the path was treated as a literal file name and not + # parsed as SQL. + download_dir = tempfile.mkdtemp() + session.file.get(special_path, download_dir) + downloaded = [ + f + for f in os.listdir(download_dir) + if os.path.isfile(os.path.join(download_dir, f)) + ] + assert len(downloaded) == 1, downloaded + with open(os.path.join(download_dir, downloaded[0])) as fh: + content = fh.read() + data_rows = [line for line in content.splitlines() if line.strip()] + # Header ("A,B") + the DataFrame's own 3 data rows == 4 lines. + assert len(data_rows) == 4, content + assert content == "A,B\none,1\ntwo,2\nthree,3\n", content diff --git a/tests/integ/scala/test_dataframe_writer_suite.py b/tests/integ/scala/test_dataframe_writer_suite.py index 835a4f3589..abdea78b3c 100644 --- a/tests/integ/scala/test_dataframe_writer_suite.py +++ b/tests/integ/scala/test_dataframe_writer_suite.py @@ -989,6 +989,55 @@ def test_writer_csv(session, temp_stage, caplog): Utils.check_answer(data7, df) +@pytest.mark.skipif( + "config.getoption('local_testing_mode', default=False)", + reason="COPY INTO is not supported in Local Testing", +) +def test_writer_csv_stage_path_escapes_special_characters(session, temp_stage): + """``DataFrame.write.csv`` routes the destination through ``normalize_path``, + which must escape both backslashes and single quotes so that a path + containing a backslash immediately followed by a single quote stays inside + the stage-location string literal in the generated ``COPY INTO`` and the + SQL is always valid. + """ + df = session.create_dataframe([[1, 2], [3, 4]], schema=["a", "b"]) + schema = StructType( + [StructField("a", IntegerType()), StructField("b", IntegerType())] + ) + + # 1) Path whose directory name contains a backslash. This must round-trip + # through both write (COPY INTO) and read (COPY INTO/SELECT). + backslash_path = f"{temp_stage}/back\\slash_dir/data.csv" + result = df.write.csv(backslash_path, single=True) + assert result[0].rows_unloaded == 2 + data = session.read.schema(schema).csv(f"@{backslash_path}") + Utils.check_answer(data, df, sort=True) + + # 2) Paths whose directory names contain a single quote and a + # backslash-quote combination. The write must succeed and the bytes must + # land verbatim on the stage (verified via LIST), confirming the path is + # treated as literal data rather than parsed as SQL. + for sub in ["o'clock", "mix\\'both"]: + path = f"{temp_stage}/{sub}/data.csv" + write_result = df.write.csv(path, single=True) + assert write_result[0].rows_unloaded == 2 + + # 3) A file name containing several special characters at once: a backslash + # followed by a single quote, parentheses, a comma and a trailing ``--``. + # These must all be treated as literal characters of the path -- the file + # is written with the DataFrame's own rows and the whole name appears + # verbatim as a single physical file on the stage. + special_name = "out\\' , (note) -- draft" + special_path = f"@{temp_stage}/{special_name}" + special_result = df.write.csv(special_path, single=True) + # The DataFrame's own rows are unloaded; the path is not parsed as SQL. + assert special_result[0].rows_unloaded == 2 + + listed = [row[0] for row in session.sql(f"LIST '@{temp_stage}'").collect()] + # The full name survives as a single physical file. + assert any(special_name in name for name in listed), listed + + @pytest.mark.skipif( "config.getoption('local_testing_mode', default=False)", reason="BUG: SNOW-1235716 should raise not implemented error not AttributeError: 'MockExecutionPlan' object has no attribute 'replace_repeated_subquery_with_cte', FEAT: parquet support", diff --git a/tests/unit/scala/test_utils_suite.py b/tests/unit/scala/test_utils_suite.py index 02ca8412e0..d226039a7a 100644 --- a/tests/unit/scala/test_utils_suite.py +++ b/tests/unit/scala/test_utils_suite.py @@ -167,7 +167,9 @@ def test_normalize_file(is_local): assert normalize_path(name2, is_local) == f"'{symbol}sta\\'ge'" name3 = "s ta\\'ge " assert normalize_path(name3, is_local) == ( - f"'{symbol}s ta/\\'ge'" if is_local and IS_WINDOWS else f"'{symbol}s ta\\\\'ge'" + f"'{symbol}s ta/\\'ge'" + if is_local and IS_WINDOWS + else f"'{symbol}s ta\\\\\\'ge'" ) diff --git a/tests/unit/test_internal_utils.py b/tests/unit/test_internal_utils.py index fcad4a21c4..b6c112dee6 100644 --- a/tests/unit/test_internal_utils.py +++ b/tests/unit/test_internal_utils.py @@ -76,6 +76,72 @@ def test_normalize_path(path: str, is_local: bool, expected: str) -> None: assert expected == actual +def _decode_snowflake_literal(literal: str) -> str: + """Simulate Snowflake's decoding of a single-quoted string literal. + + Snowflake treats ``\\`` as an escape character inside a single-quoted literal, + so ``\\\\`` decodes to one backslash and ``\\'`` decodes to one single quote. + An unescaped single quote closes the literal. This helper returns the decoded + literal value and raises if the literal is closed early -- which would mean the + path was not escaped correctly and the generated SQL is invalid. + """ + assert literal.startswith("'") and literal.endswith( + "'" + ), f"not a quoted literal: {literal!r}" + body = literal[1:-1] + out = [] + i = 0 + while i < len(body): + ch = body[i] + if ch == "\\" and i + 1 < len(body): + out.append(body[i + 1]) + i += 2 + elif ch == "'": + raise AssertionError( + f"unescaped quote closes literal early at index {i}: {literal!r}" + ) + else: + out.append(ch) + i += 1 + return "".join(out) + + +@pytest.mark.parametrize("is_local", [True, False]) +@pytest.mark.parametrize( + "raw_path", + [ + # Paths containing a backslash immediately followed by a single quote, + # plus parentheses, commas and a trailing ``--``. Before the fix the + # backslash was not escaped, so ``\'`` was written as ``\\'`` and closed + # the literal early, producing invalid SQL. + "@~/out\\' , (note) FILE_FORMAT=(TYPE=CSV) -- draft", + "report\\' , (v2) draft --", + # Plain special characters that must round-trip as literal data. + "@stage/o'clock/file.csv", + "@stage/back\\slash/file.csv", + "@stage/double\\\\back/file.csv", + '@stage/dquote"/file.csv', + "@stage/uniƩcode/file.csv", + "@stage/all\\'\"mix/file.csv", + ], +) +def test_normalize_path_escapes_backslash_and_quote(raw_path, is_local): + """``normalize_path`` must produce a Snowflake string literal that decodes back + to the original path. A backslash followed by a single quote must stay inside + the literal and not close it early, so the generated SQL is always valid and + the path is treated as literal data.""" + literal = utils.normalize_path(raw_path, is_local) + # The output must be a well-formed single-quoted literal: decoding it must not + # raise (i.e. the literal is not closed early). + decoded = _decode_snowflake_literal(literal) + # The decoded literal must end with the (stripped) raw path -- the prefix may + # differ only by an added ``@`` / ``file://`` scheme prefix. + expected_tail = raw_path.strip() + assert decoded.endswith( + expected_tail + ), f"decoded={decoded!r} does not end with {expected_tail!r}" + + def test__pandas_importer(): imported_pandas = _pandas_importer() try: From 1c58404d74afc79fbf0c0a99c566464f7c802ce1 Mon Sep 17 00:00:00 2001 From: Adam Ling Date: Wed, 1 Jul 2026 20:25:29 +0000 Subject: [PATCH 2/4] SNOW-3718333: fix tests for platform/stage-storage behavior The escaping fix is correct; two newly-added tests encoded assumptions that don't hold in CI: - test_normalize_path_escapes_backslash_and_quote asserted backslashes round-trip for is_local=True, but on Windows local paths have backslashes normalized to '/' before escaping (pre-existing behavior). Mirror that transform in the expected value; the early-termination guarantee is still checked on every platform. - test_writer_csv_stage_path_escapes_special_characters read back a backslash-containing stage path, but a literal backslash is not preserved as a directory separator by stage storage. Assert the writes succeed (valid SQL, path treated as literal data) instead of a read-back round-trip. Co-Authored-By: Claude Opus 4.8 --- .../scala/test_dataframe_writer_suite.py | 58 ++++++++----------- tests/unit/test_internal_utils.py | 6 ++ 2 files changed, 30 insertions(+), 34 deletions(-) diff --git a/tests/integ/scala/test_dataframe_writer_suite.py b/tests/integ/scala/test_dataframe_writer_suite.py index abdea78b3c..190af4aaa2 100644 --- a/tests/integ/scala/test_dataframe_writer_suite.py +++ b/tests/integ/scala/test_dataframe_writer_suite.py @@ -999,43 +999,33 @@ def test_writer_csv_stage_path_escapes_special_characters(session, temp_stage): containing a backslash immediately followed by a single quote stays inside the stage-location string literal in the generated ``COPY INTO`` and the SQL is always valid. + + Each write below uses a path with characters that, before the fix, would + close the location string literal early and produce invalid SQL (a + backslash, a single quote, a ``\\'`` combination, parentheses, a comma and a + trailing ``--``). The writes must now succeed with the DataFrame's own rows + unloaded, which proves the path is escaped as literal data and not parsed as + SQL. Note: a literal backslash is not preserved as a directory separator by + stage storage, so we assert the write succeeds rather than a read-back + round-trip. """ df = session.create_dataframe([[1, 2], [3, 4]], schema=["a", "b"]) - schema = StructType( - [StructField("a", IntegerType()), StructField("b", IntegerType())] - ) - # 1) Path whose directory name contains a backslash. This must round-trip - # through both write (COPY INTO) and read (COPY INTO/SELECT). - backslash_path = f"{temp_stage}/back\\slash_dir/data.csv" - result = df.write.csv(backslash_path, single=True) - assert result[0].rows_unloaded == 2 - data = session.read.schema(schema).csv(f"@{backslash_path}") - Utils.check_answer(data, df, sort=True) - - # 2) Paths whose directory names contain a single quote and a - # backslash-quote combination. The write must succeed and the bytes must - # land verbatim on the stage (verified via LIST), confirming the path is - # treated as literal data rather than parsed as SQL. - for sub in ["o'clock", "mix\\'both"]: - path = f"{temp_stage}/{sub}/data.csv" - write_result = df.write.csv(path, single=True) - assert write_result[0].rows_unloaded == 2 - - # 3) A file name containing several special characters at once: a backslash - # followed by a single quote, parentheses, a comma and a trailing ``--``. - # These must all be treated as literal characters of the path -- the file - # is written with the DataFrame's own rows and the whole name appears - # verbatim as a single physical file on the stage. - special_name = "out\\' , (note) -- draft" - special_path = f"@{temp_stage}/{special_name}" - special_result = df.write.csv(special_path, single=True) - # The DataFrame's own rows are unloaded; the path is not parsed as SQL. - assert special_result[0].rows_unloaded == 2 - - listed = [row[0] for row in session.sql(f"LIST '@{temp_stage}'").collect()] - # The full name survives as a single physical file. - assert any(special_name in name for name in listed), listed + special_paths = [ + # Directory name containing a backslash. + f"{temp_stage}/back\\slash_dir/data.csv", + # Directory name containing a single quote. + f"{temp_stage}/o'clock/data.csv", + # Directory name containing a backslash immediately followed by a quote. + f"{temp_stage}/mix\\'both/data.csv", + # File name mixing a backslash-quote, parentheses, a comma and a + # trailing ``--`` -- all must be treated as literal path characters. + f"@{temp_stage}/out\\' , (note) -- draft", + ] + for path in special_paths: + result = df.write.csv(path, single=True) + # The DataFrame's own rows are unloaded; the path is not parsed as SQL. + assert result[0].rows_unloaded == 2, path @pytest.mark.skipif( diff --git a/tests/unit/test_internal_utils.py b/tests/unit/test_internal_utils.py index b6c112dee6..f09cfc63d3 100644 --- a/tests/unit/test_internal_utils.py +++ b/tests/unit/test_internal_utils.py @@ -137,6 +137,12 @@ def test_normalize_path_escapes_backslash_and_quote(raw_path, is_local): # The decoded literal must end with the (stripped) raw path -- the prefix may # differ only by an added ``@`` / ``file://`` scheme prefix. expected_tail = raw_path.strip() + # Local paths on Windows are normalized (backslashes -> forward slashes) + # before escaping, so mirror that transform here. This only affects the + # round-trip comparison; the escaping guarantee checked above (the literal + # never closes early) still holds for every input on every platform. + if is_local and utils.OPERATING_SYSTEM == "Windows": + expected_tail = expected_tail.replace("\\", "/") assert decoded.endswith( expected_tail ), f"decoded={decoded!r} does not end with {expected_tail!r}" From 51980d7feae4bc99041a853040e43af6fef9b64b Mon Sep 17 00:00:00 2001 From: Adam Ling Date: Wed, 1 Jul 2026 21:17:18 +0000 Subject: [PATCH 3/4] SNOW-3718333: clarify path-escaping with named constants Use BACKSLASH/SINGLE_QUOTE constants for the escape replacements and trim the comment. Behavior is unchanged; this only removes the Python escape double-counting that made the original one-liner hard to read. Co-Authored-By: Claude Opus 4.8 --- src/snowflake/snowpark/_internal/utils.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/snowflake/snowpark/_internal/utils.py b/src/snowflake/snowpark/_internal/utils.py index d2d258d759..ee28984898 100644 --- a/src/snowflake/snowpark/_internal/utils.py +++ b/src/snowflake/snowpark/_internal/utils.py @@ -429,13 +429,17 @@ def normalize_path(path: str, is_local: bool) -> str: return path if is_local and OPERATING_SYSTEM == "Windows": path = path.replace("\\", "/") - # Escape characters that are special inside a Snowflake single-quoted string - # literal. Backslash must be escaped before the single quote: otherwise a - # path containing ``\'`` would be written as ``\\'``, which Snowflake decodes - # as ``\`` followed by an unescaped quote that closes the literal early and - # produces invalid SQL. Escaping the backslash first keeps the path a single - # literal value. - path = path.strip().replace("\\", "\\\\").replace("'", "\\'") + # Escape the backslash before the single quote so the path stays a single + # Snowflake string literal; the reverse order would let an escaped quote + # close the literal early and produce invalid SQL. Constants keep the + # replacements readable (no Python escape double-counting). + BACKSLASH = "\\" + SINGLE_QUOTE = "'" + path = ( + path.strip() + .replace(BACKSLASH, BACKSLASH * 2) # \ -> \\ + .replace(SINGLE_QUOTE, BACKSLASH + SINGLE_QUOTE) # ' -> \' + ) if not any(path.startswith(prefix) for prefix in prefixes): path = f"{prefixes[0]}{path}" return f"'{path}'" From bfa100b2a7f59bc8d62f5a04ad952aa4104d5e01 Mon Sep 17 00:00:00 2001 From: Adam Ling Date: Thu, 2 Jul 2026 05:11:03 +0000 Subject: [PATCH 4/4] SNOW-3718333: fix modin to_csv test for stage-storage backslash behavior Stage storage does not preserve a literal backslash as a path-separator character, so asserting the round-tripped name endswith "o'clock\dir/..." always fails. Switch part (a) to a plain single-quote path that does round-trip verbatim; keep part (b) asserting only that the write succeeds (valid SQL), not the exact name on LIST. Co-Authored-By: Claude Sonnet 4.6 --- tests/integ/modin/io/test_to_csv.py | 46 ++++++++++++++--------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/tests/integ/modin/io/test_to_csv.py b/tests/integ/modin/io/test_to_csv.py index cdfab5735c..34c2befc7c 100644 --- a/tests/integ/modin/io/test_to_csv.py +++ b/tests/integ/modin/io/test_to_csv.py @@ -309,34 +309,20 @@ def test_to_csv_stage_path_escapes_special_characters(sf_stage, session): # None index name is not supported when writing to a Snowflake stage. snow_df.index.set_names(["X"], inplace=True) - # (a) Stage path whose directory name contains a single quote and a - # backslash. The write must succeed and the bytes must land verbatim on - # the stage (the path is data, not SQL). ``to_csv`` to a stage emits one - # query (the COPY INTO). - legit_name = "o'clock\\dir/mods.csv" - legit_path = f"@{sf_stage}/{legit_name}" + # (a) Stage path whose directory name contains a single quote. The quote is + # escaped as literal data, so the write succeeds and the file lands under + # that exact name. ``to_csv`` to a stage emits one query (the COPY INTO); + # downloading it back confirms the path was treated as a literal file + # name and not parsed as SQL. + quote_name = "o'clock/mods.csv" + quote_path = f"@{sf_stage}/{quote_name}" with SqlCounter(query_count=1): - snow_df.to_csv(legit_path, index=False) + snow_df.to_csv(quote_path, index=False) listed = [row[0] for row in session.sql(f"LIST '@{sf_stage}'").collect()] - assert any(name.endswith("o'clock\\dir/mods.csv") for name in listed), listed + assert any(name.endswith(quote_name) for name in listed), listed - # (b) A file name containing several special characters at once: a backslash - # followed by a single quote, parentheses, a comma and a trailing ``--``. - # These must all be treated as literal characters of the path. - special_name = "report\\' , (note) -- draft" - special_path = f"@{sf_stage}/{special_name}" - with SqlCounter(query_count=1): - snow_df.to_csv(special_path, index=False) - - # The whole name survives as a single physical file. - listed = [row[0] for row in session.sql(f"LIST '@{sf_stage}'").collect()] - assert any(special_name in name for name in listed), listed - - # Download the file and verify its contents are exactly the DataFrame's own - # rows -- confirming the path was treated as a literal file name and not - # parsed as SQL. download_dir = tempfile.mkdtemp() - session.file.get(special_path, download_dir) + session.file.get(quote_path, download_dir) downloaded = [ f for f in os.listdir(download_dir) @@ -349,3 +335,15 @@ def test_to_csv_stage_path_escapes_special_characters(sf_stage, session): # Header ("A,B") + the DataFrame's own 3 data rows == 4 lines. assert len(data_rows) == 4, content assert content == "A,B\none,1\ntwo,2\nthree,3\n", content + + # (b) A file name mixing a backslash, a single quote, parentheses, a comma + # and a trailing ``--`` must produce valid SQL: before the fix the + # unescaped backslash/quote closed the location literal early. The write + # must succeed with a single COPY INTO query -- if the path were parsed as + # SQL the statement would error instead. Stage storage does not preserve a + # literal backslash as a path character, so we assert the write succeeds + # rather than reading back the exact name. + special_name = "report\\' , (note) -- draft" + special_path = f"@{sf_stage}/{special_name}" + with SqlCounter(query_count=1): + snow_df.to_csv(special_path, index=False)