feat: add DataFrame checkpoint and local checkpoint support - #1969
feat: add DataFrame checkpoint and local checkpoint support#1969lonless9 wants to merge 49 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Adds Spark Connect support for DataFrame checkpoint() / localCheckpoint() in Sail by materializing or deferring checkpoint evaluation into a server-side cached relation registry, enabling later queries to reference checkpointed results via CachedRemoteRelation.
Changes:
- Implement
CheckpointCommandandRemoveCachedRemoteRelationCommandhandling on the Spark Connect server, backed by a newCachedRelationRegistry. - Add
CachedRemoteRelationresolution in the plan resolver and preserve field metadata when renaming projected schemas. - Add Python test coverage for eager/lazy checkpointing behavior and mark compatibility entries as supported.
Reviewed changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| python/pysail/tests/spark/test_dataframe.py | Adds tests covering eager/lazy checkpoint semantics, source removal survival, and storage-level handling. |
| python/pysail/data/compatibility/dataframe.json | Marks checkpoint / localCheckpoint as supported with notes. |
| crates/sail-spark-connect/src/service/plan_executor.rs | Implements checkpoint execution, checkpoint path validation, and cached-relation removal command handling. |
| crates/sail-spark-connect/src/server.rs | Wires RemoveCachedRemoteRelationCommand to the service handler. |
| crates/sail-session/src/session_factory/server.rs | Registers CachedRelationRegistry as a session extension. |
| crates/sail-plan/src/resolver/query/mod.rs | Resolves CachedRemoteRelation by fetching from the registry and producing a logical plan. |
| crates/sail-plan/src/lib.rs | Exposes resolve_logical_plan helper for lazy checkpoint creation. |
| crates/sail-common-datafusion/src/rename/logical_plan.rs | Preserves field metadata when aliasing/renaming logical plan columns. |
| crates/sail-common-datafusion/src/lib.rs | Exposes the new cached_relation module. |
| crates/sail-common-datafusion/src/cached_relation.rs | Introduces cached relation types, materialization for local/reliable checkpoints, and checkpoint cleanup utilities. |
| crates/sail-common-datafusion/Cargo.toml | Adds object_store and parquet dependencies for checkpoint IO/cleanup. |
| Cargo.lock | Updates lockfile for the new dependencies. |
Spark 3.5.7 Test ReportCommit Information
Test Summary
Test DetailsError CountsPassed Tests Diff(empty) Failed Tests |
Spark 4.1.1 Test ReportCommit Information
Test Summary
Test DetailsError CountsPassed Tests Diff--- before.txt 2026-07-17 07:08:07.392095055 +0000
+++ after.txt 2026-07-17 07:08:08.135112715 +0000
@@ -93,0 +94 @@
+pyspark/sql/dataframe.py::pyspark.sql.dataframe.DataFrame.localCheckpoint
@@ -1689,0 +1691 @@
+pyspark/sql/tests/connect/test_parity_dataframe.py::DataFrameParityTests::test_local_checkpoint_dataframe_with_storage_levelFailed Tests(truncated) |
Ibis Test ReportCommit Information
Test Summary
Test DetailsError CountsPassed Tests Diff--- before.txt 2026-07-17 07:06:53.023453121 +0000
+++ after.txt 2026-07-17 07:06:53.230456847 +0000
@@ -532 +531,0 @@
-ibis/backends/tests/test_export.py::test_table_to_csv[pyspark]Failed Tests |
Codecov Report❌ Patch coverage is @@ Coverage Diff @@
## main #1969 +/- ##
==========================================
- Coverage 79.42% 77.83% -1.59%
==========================================
Files 906 912 +6
Lines 171251 174459 +3208
==========================================
- Hits 136014 135796 -218
- Misses 35237 38663 +3426
*This pull request uses carry forward flags. Click here to find out more.
... and 82 files with indirect coverage changes 🚀 New features to boost your workflow:
|
# Conflicts: # Cargo.lock
# Conflicts: # scripts/spark-tests/plugins/spark.py
# Conflicts: # crates/sail-spark-connect/src/service/plan_executor.rs
linhr
left a comment
There was a problem hiding this comment.
I feel we need to redo the implementation once we figure out the design.
I thought we were going to do checkpoint based on object storage only. We do not want to follow Spark's semantics around "local" checkpoint or serialization level. It seems the current implementation sends all data to the driver and stores it in the driver's memory or disk, which is not scalable so it does not have high practical value. (Of course, we do not want to store the data in the worker either, since the worker is stateless for the simplicity of auto-scaling.)
When we work with Codex to do the design, the priority is to achieving a simple but effective solution that meet the user's need in cloud environments. If Codex instead attempts to fully match Spark's behavior and favors feature parity in tests, the overall architecture may be complex and it's hard to reason about the performance characteristics.
I wonder if we should only implement remote eager checkpoint in the first iteration, with simple cleanup logic at the end of the session. I feel the cached relation lease is hard to reason about. Lazy checkpoint is a bit tricky as well, since we allow concurrent query planning and execution within the session, so which query is responsible for materialization requires careful design.
| fn is_exclusively_owned(&self) -> bool { | ||
| Arc::strong_count(&self.data) == 1 | ||
| } |
There was a problem hiding this comment.
A method like this doesn't seem right. The usage is likely vulnerable to race conditions.
| let bytes = write_record_batches_file(&batches, input_schema.as_ref())?; | ||
| store | ||
| .put(&location, PutPayload::from(bytes)) | ||
| .await | ||
| .map_err(|error| DataFusionError::ObjectStore(Box::new(error)))?; |
There was a problem hiding this comment.
This materializes everything in memory. We should stream the data and use multipart upload instead.
Moved to #2270