SNOW-3472759: support for to_polars() for snowpark -> polars interoperability - #4288
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 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. 🚀 New features to boost your workflow:
|
…lelize file open and read for parquet, to_arrow for plans with 1 query
…row_batches; 3. add usage notes in docstring
| ) | ||
| # 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( |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
| rid = uuid.uuid4().hex[:12] | ||
| stage = df._session.get_session_stage().rstrip("/") | ||
| sub = f"{stage}/{sub_prefix}/{rid}/" | ||
| df.write.copy_into_location( |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
get_session_stage() returns a temporary stage that Snowflake auto-drops on session close, so no need to recycle here
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
I remember that List would not return fully qualified stage name, should we make it fully qualified before return?
There was a problem hiding this comment.
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.
13c8011 to
5ad5fd8
Compare
Which Jira issue is this PR addressing? Make sure that there is an accompanying issue to your PR.
Fixes SNOW-3472759
Fill out the following pre-review checklist:
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