Skip to content

SNOW-3472759: support for to_polars() for snowpark -> polars interoperability - #4288

Merged
sfc-gh-mayliu merged 12 commits into
mainfrom
SNOW-3472759-support-to-polars
Jul 30, 2026
Merged

SNOW-3472759: support for to_polars() for snowpark -> polars interoperability#4288
sfc-gh-mayliu merged 12 commits into
mainfrom
SNOW-3472759-support-to-polars

Conversation

@sfc-gh-mayliu

@sfc-gh-mayliu sfc-gh-mayliu commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator
  1. Which Jira issue is this PR addressing? Make sure that there is an accompanying issue to your PR.

    Fixes SNOW-3472759

  2. Fill out the following pre-review checklist:

    • I am adding a new automated test(s) to verify correctness of my new code
      • If this test skips Local Testing mode, I'm requesting review from @snowflakedb/local-testing
    • I am adding new logging messages
    • I am adding a new telemetry message
    • I am adding new credentials
    • I am adding a new dependency
    • If this is a new feature/behavior, I'm adding the Local Testing parity changes.
    • I acknowledge that I have ensured my changes to be thread-safe. Follow the link for more information: Thread-safe Developer Guidelines
    • If adding any arguments to public Snowpark APIs or creating new public Snowpark APIs, I acknowledge that I have ensured my changes include AST support. Follow the link for more information: AST Support Guidelines
  3. Please describe how your code solves the related issue.

    Please write a short description of how your code change solves the related issue.

This PR adds DataFrame.to_polars() to enable direct conversion from a Snowpark DataFrame to a Polars DataFrame/LazyFrame

@codecov-commenter

codecov-commenter commented Jul 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.47%. Comparing base (2b9291a) to head (5ad5fd8).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4288      +/-   ##
==========================================
+ Coverage   95.26%   95.47%   +0.20%     
==========================================
  Files         171      172       +1     
  Lines       44720    44783      +63     
  Branches     7676     7685       +9     
==========================================
+ Hits        42603    42757     +154     
+ Misses       1300     1253      -47     
+ Partials      817      773      -44     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@sfc-gh-mayliu
sfc-gh-mayliu requested a review from a team as a code owner July 20, 2026 23:20
Comment thread src/snowflake/snowpark/dataframe.py Outdated
@sfc-gh-mayliu
sfc-gh-mayliu requested a review from a team July 21, 2026 18:35
Comment thread src/snowflake/snowpark/dataframe.py Outdated

@snowflake-security-bot snowflake-security-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Snowflake Security Review

Security grade: A — Passed

No security findings after adjudication. This PR passes the Snowflake Security Review.

📊 5 of 6 files (1 test skipped) · 11,645 lines reviewed · 4 candidates → 0 kept · retrieval: on

)
# The stream objects are owned by the returned LazyFrame; Polars closes them
# when the scan is materialized (or if the LazyFrame is discarded).
streams = _open_stage_files_parallel(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i have a question about this ingestion path.
Since this path is ingesting with a stream of parquet file,
if I call collect() two times, will the second collect() still be able to fetch anything since the stream is already consumed at that time?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both session.file.get_stream and SnowflakeFile.open eagerly downloads/buffers, so the multiple Polars .collect() calls are safe to get the predicate/projection pruned data on CPU without needing to re-download. However, it's a Snowflake infra-limitation that network I/O pruning costs can't be saved due to the lack of HTTP Range GET support in session.file and SnowflakeFile

Tested with 1M rows: first .collect() takes 0.04s, second .collect() takes 0.03s — effectively the same cpu-decode cost with no additional network I/O.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this sounds good!

rid = uuid.uuid4().hex[:12]
stage = df._session.get_session_stage().rstrip("/")
sub = f"{stage}/{sub_prefix}/{rid}/"
df.write.copy_into_location(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it appears like the uploaded parquet file is never removed, do we need a garbage cleaning mechanism to make sure the stage is not filled when user invoke this ingestion route multiple times?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_session_stage() returns a temporary stage that Snowflake auto-drops on session close, so no need to recycle here

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am thinking about a scenario in which user have a long running pipeline that call to_polars() in this ingestion route, in that scenario, the stage will keep accumulating uploaded parquet files. Do we need to worry about this scenario or is this not a expected usage?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question, discussed offline. TLDR: long-lived sessions exist but are rare, and there's no storage limit on temp stages. sproc also rejects REMOVE {file} calls, so a clean up would result in inconsistency between local and server environments. We will rely on temp stages' property to clean up after itself, similar to existing Snowpark patterns

overwrite=True,
statement_params=statement_params,
)
rows = df._session.sql(f"LIST '{sub}'").collect(statement_params=statement_params)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember that List would not return fully qualified stage name, should we make it fully qualified before return?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, LS returns stage-relative paths and we should make it fully qualified. We already have the fully-qualified sub prefix at hand, so the fix is to extract just the filename from the LIST result and prepend sub instead. Fix incoming.

@snowflake-security-bot snowflake-security-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Snowflake Security Review

Security grade: A — Passed

No security findings after adjudication. This PR passes the Snowflake Security Review.

📊 5 of 6 files (1 test skipped) · 11,646 lines reviewed · 4 candidates → 0 kept · retrieval: on

Comment thread src/snowflake/snowpark/_internal/polars_backend.py Outdated
Comment thread src/snowflake/snowpark/dataframe.py Outdated
Comment thread src/snowflake/snowpark/dataframe.py
@sfc-gh-mayliu
sfc-gh-mayliu force-pushed the SNOW-3472759-support-to-polars branch from 13c8011 to 5ad5fd8 Compare July 30, 2026 17:44
@sfc-gh-mayliu
sfc-gh-mayliu requested review from a team and removed request for sfc-gh-helmeleegy July 30, 2026 17:46

@snowflake-security-bot snowflake-security-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Snowflake Security Review

Security grade: A — Passed

No security findings after adjudication. This PR passes the Snowflake Security Review.

📊 5 of 6 files (1 test skipped) · 11,623 lines reviewed · 2 candidates → 0 kept · retrieval: on

@snowflake-security-bot snowflake-security-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Snowflake Security Review

Security grade: A — Passed

No security findings after adjudication. This PR passes the Snowflake Security Review.

📊 5 of 6 files (1 test skipped) · 11,623 lines reviewed · 2 candidates → 1 kept · retrieval: on

@sfc-gh-mayliu
sfc-gh-mayliu merged commit cd6d20b into main Jul 30, 2026
51 of 54 checks passed
@sfc-gh-mayliu
sfc-gh-mayliu deleted the SNOW-3472759-support-to-polars branch July 30, 2026 21:12
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 30, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants