This is an automated email from the ASF dual-hosted git repository.

JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-rust.git


The following commit(s) were added to refs/heads/main by this push:
     new f07703a6 feat(datafusion): configure SQLContext runtime resources 
(#675)
f07703a6 is described below

commit f07703a6f3d7edbe3410f4efd3038e23f775b42f
Author: shyjsarah <[email protected]>
AuthorDate: Wed Aug 5 11:44:49 2026 +0800

    feat(datafusion): configure SQLContext runtime resources (#675)
---
 .../python/python/pypaimon_rust/datafusion.pyi     | 12 ++-
 bindings/python/src/context.rs                     | 81 ++++++++++++++++++-
 bindings/python/tests/test_datafusion.py           | 42 ++++++++++
 crates/integrations/datafusion/src/lib.rs          |  2 +-
 crates/integrations/datafusion/src/sql_context.rs  | 93 +++++++++++++++++++---
 docs/src/python-binding.md                         | 24 ++++++
 6 files changed, 239 insertions(+), 15 deletions(-)

diff --git a/bindings/python/python/pypaimon_rust/datafusion.pyi 
b/bindings/python/python/pypaimon_rust/datafusion.pyi
index 07586e63..a43e57f2 100644
--- a/bindings/python/python/pypaimon_rust/datafusion.pyi
+++ b/bindings/python/python/pypaimon_rust/datafusion.pyi
@@ -15,7 +15,8 @@
 # specific language governing permissions and limitations
 # under the License.
 
-from typing import Any, Callable, Dict, List, Optional, Sequence, TypeAlias, 
Union
+from os import PathLike
+from typing import Any, Callable, Dict, List, Literal, Optional, Sequence, 
TypeAlias, Union
 
 import pyarrow
 
@@ -185,7 +186,14 @@ def udf(
     ...
 
 class SQLContext:
-    def __init__(self) -> None: ...
+    def __init__(
+        self,
+        *,
+        memory_pool_type: Optional[Literal["fair", "greedy"]] = None,
+        memory_pool_bytes: Optional[int] = None,
+        temp_directory: Optional[Union[str, PathLike[str]]] = None,
+        max_temp_directory_size_bytes: Optional[int] = None,
+    ) -> None: ...
     def register_catalog(
         self, catalog_name: str, catalog_options: Dict[str, str]
     ) -> None: ...
diff --git a/bindings/python/src/context.rs b/bindings/python/src/context.rs
index 2536c5d5..6ab04ad7 100644
--- a/bindings/python/src/context.rs
+++ b/bindings/python/src/context.rs
@@ -16,6 +16,7 @@
 // under the License.
 
 use std::collections::HashMap;
+use std::path::PathBuf;
 use std::sync::Arc;
 
 use arrow::compute::cast;
@@ -23,6 +24,8 @@ use arrow::datatypes::{DataType as ArrowDataType, Field as 
ArrowField};
 use arrow::pyarrow::{FromPyArrow, ToPyArrow};
 use arrow::record_batch::{RecordBatch, RecordBatchOptions};
 use datafusion::catalog::CatalogProvider;
+use datafusion::execution::memory_pool::{FairSpillPool, GreedyMemoryPool, 
MemoryPool};
+use datafusion::execution::runtime_env::RuntimeEnvBuilder;
 use datafusion::logical_expr::{Signature, TypeSignature, Volatility};
 use datafusion_ffi::catalog_provider::FFI_CatalogProvider;
 use datafusion_ffi::proto::logical_extension_codec::FFI_LogicalExtensionCodec;
@@ -181,6 +184,62 @@ pub struct PySQLContext {
 }
 
 impl PySQLContext {
+    fn build_sql_context(
+        memory_pool_type: Option<String>,
+        memory_pool_bytes: Option<usize>,
+        temp_directory: Option<PathBuf>,
+        max_temp_directory_size_bytes: Option<u64>,
+    ) -> PyResult<SQLContext> {
+        if memory_pool_type.is_some() && memory_pool_bytes.is_none() {
+            return Err(PyValueError::new_err(
+                "memory_pool_type requires memory_pool_bytes",
+            ));
+        }
+        if memory_pool_bytes == Some(0) {
+            return Err(PyValueError::new_err(
+                "memory_pool_bytes must be greater than zero",
+            ));
+        }
+        if temp_directory
+            .as_ref()
+            .is_some_and(|path| path.as_os_str().is_empty())
+        {
+            return Err(PyValueError::new_err("temp_directory must not be 
empty"));
+        }
+
+        let has_runtime_config = memory_pool_bytes.is_some()
+            || temp_directory.is_some()
+            || max_temp_directory_size_bytes.is_some();
+        if !has_runtime_config {
+            return Ok(SQLContext::new());
+        }
+
+        let mut runtime_builder = RuntimeEnvBuilder::new();
+        if let Some(memory_pool_bytes) = memory_pool_bytes {
+            let memory_pool: Arc<dyn MemoryPool> =
+                match memory_pool_type.as_deref().unwrap_or("greedy") {
+                    "fair" => Arc::new(FairSpillPool::new(memory_pool_bytes)),
+                    "greedy" => 
Arc::new(GreedyMemoryPool::new(memory_pool_bytes)),
+                    other => {
+                        return Err(PyValueError::new_err(format!(
+                            "unsupported memory_pool_type '{other}'; expected 
'fair' or 'greedy'"
+                        )));
+                    }
+                };
+            runtime_builder = runtime_builder.with_memory_pool(memory_pool);
+        }
+        if let Some(temp_directory) = temp_directory {
+            runtime_builder = 
runtime_builder.with_temp_file_path(temp_directory);
+        }
+        if let Some(max_temp_directory_size_bytes) = 
max_temp_directory_size_bytes {
+            runtime_builder =
+                
runtime_builder.with_max_temp_directory_size(max_temp_directory_size_bytes);
+        }
+
+        let runtime_env = runtime_builder.build_arc().map_err(df_to_py_err)?;
+        Ok(SQLContext::builder().with_runtime_env(runtime_env).build())
+    }
+
     fn vector_float32_type() -> ArrowDataType {
         ArrowDataType::List(Arc::new(ArrowField::new(
             "item",
@@ -325,9 +384,27 @@ impl PySQLContext {
 #[pymethods]
 impl PySQLContext {
     #[new]
-    fn new(py: Python<'_>) -> PyResult<Self> {
+    /// Creates a Paimon SQL context.
+    ///
+    /// `memory_pool_type` accepts `"greedy"` or `"fair"` and requires
+    /// `memory_pool_bytes`. When only `memory_pool_bytes` is provided, the
+    /// greedy pool is used. Temporary files can be directed to
+    /// `temp_directory` and bounded with `max_temp_directory_size_bytes`.
+    #[pyo3(signature = (*, memory_pool_type=None, memory_pool_bytes=None, 
temp_directory=None, max_temp_directory_size_bytes=None))]
+    fn new(
+        py: Python<'_>,
+        memory_pool_type: Option<String>,
+        memory_pool_bytes: Option<usize>,
+        temp_directory: Option<PathBuf>,
+        max_temp_directory_size_bytes: Option<u64>,
+    ) -> PyResult<Self> {
         let ctx = Self {
-            inner: SQLContext::new(),
+            inner: Self::build_sql_context(
+                memory_pool_type,
+                memory_pool_bytes,
+                temp_directory,
+                max_temp_directory_size_bytes,
+            )?,
         };
         if let Err(err) = ctx.register_multimodal_builtins(py) {
             Self::warn_multimodal_builtin_registration_failure(py, err);
diff --git a/bindings/python/tests/test_datafusion.py 
b/bindings/python/tests/test_datafusion.py
index 916e1cc3..cfaa1a0e 100644
--- a/bindings/python/tests/test_datafusion.py
+++ b/bindings/python/tests/test_datafusion.py
@@ -18,6 +18,7 @@
 import io
 import json
 import os
+import re
 import struct
 import sys
 import tempfile
@@ -83,6 +84,47 @@ def extract_rows(batches):
     return sorted(zip(table["id"].to_pylist(), table["name"].to_pylist()))
 
 
+def test_sql_context_accepts_runtime_resource_configuration():
+    with tempfile.TemporaryDirectory() as temp_directory:
+        ctx = SQLContext(
+            memory_pool_type="fair",
+            memory_pool_bytes=16 * 1024 * 1024,
+            temp_directory=temp_directory,
+            max_temp_directory_size_bytes=256 * 1024 * 1024,
+        )
+
+        batches = ctx.sql(
+            """
+            EXPLAIN ANALYZE
+            SELECT value
+            FROM generate_series(1, 1000000) AS t(value)
+            ORDER BY value DESC
+            """
+        )
+        plan = pa.Table.from_batches(batches)["plan"].to_pylist()[0]
+        spill_count = re.search(r"spill_count=(\d+)", plan)
+
+        assert spill_count is not None
+        assert int(spill_count.group(1)) > 0
+        assert any(Path(temp_directory).iterdir())
+
+
[email protected](
+    ("kwargs", "message"),
+    [
+        ({"memory_pool_type": "fair"}, "requires memory_pool_bytes"),
+        ({"memory_pool_bytes": 0}, "must be greater than zero"),
+        (
+            {"memory_pool_type": "unknown", "memory_pool_bytes": 1024},
+            "expected 'fair' or 'greedy'",
+        ),
+    ],
+)
+def test_sql_context_rejects_invalid_runtime_resource_configuration(kwargs, 
message):
+    with pytest.raises(ValueError, match=message):
+        SQLContext(**kwargs)
+
+
 def test_video_snapshot_builtin_registered_on_context_init():
     ctx = SQLContext()
 
diff --git a/crates/integrations/datafusion/src/lib.rs 
b/crates/integrations/datafusion/src/lib.rs
index 05e4037a..d9044fbc 100644
--- a/crates/integrations/datafusion/src/lib.rs
+++ b/crates/integrations/datafusion/src/lib.rs
@@ -81,7 +81,7 @@ pub use full_text_search::{register_full_text_search, 
FullTextSearchFunction};
 pub use hybrid_search::{register_hybrid_search, HybridSearchFunction};
 pub use physical_plan::PaimonTableScan;
 pub use relation_planner::PaimonRelationPlanner;
-pub use sql_context::SQLContext;
+pub use sql_context::{SQLContext, SQLContextBuilder};
 pub use table::PaimonTableProvider;
 pub use variant_functions::register_variant_functions;
 pub use vector_search::{register_vector_search, VectorSearchFunction};
diff --git a/crates/integrations/datafusion/src/sql_context.rs 
b/crates/integrations/datafusion/src/sql_context.rs
index cd54a08e..4c39c49a 100644
--- a/crates/integrations/datafusion/src/sql_context.rs
+++ b/crates/integrations/datafusion/src/sql_context.rs
@@ -55,6 +55,7 @@ use datafusion::common::tree_node::{TreeNode, 
TreeNodeRecursion};
 use datafusion::common::TableReference;
 use datafusion::datasource::{MemTable, TableProvider};
 use datafusion::error::{DataFusionError, Result as DFResult};
+use datafusion::execution::runtime_env::RuntimeEnv;
 use datafusion::execution::SessionStateBuilder;
 use datafusion::logical_expr::{Expr as LogicalExpr, LogicalPlan, Volatility};
 use datafusion::prelude::{DataFrame, SessionContext};
@@ -101,16 +102,50 @@ pub struct SQLContext {
     blob_reader_registry: BlobReaderRegistry,
 }
 
-impl Default for SQLContext {
-    fn default() -> Self {
-        Self::new()
-    }
+/// Builder for [`SQLContext`].
+///
+/// The builder preserves Paimon's session configuration while allowing callers
+/// to customize DataFusion runtime resources such as memory pools, temporary
+/// directories, and object store registries.
+///
+/// # Example
+///
+/// ```ignore
+/// use std::sync::Arc;
+///
+/// use datafusion::execution::memory_pool::FairSpillPool;
+/// use datafusion::execution::runtime_env::RuntimeEnvBuilder;
+/// use paimon_datafusion::SQLContext;
+///
+/// let runtime_env = RuntimeEnvBuilder::new()
+///     .with_memory_pool(Arc::new(FairSpillPool::new(512 * 1024 * 1024)))
+///     .with_temp_file_path("/tmp/paimon-spill")
+///     .with_max_temp_directory_size(4 * 1024 * 1024 * 1024)
+///     .build_arc()?;
+/// let ctx = SQLContext::builder()
+///     .with_runtime_env(runtime_env)
+///     .build();
+/// ```
+#[derive(Default)]
+pub struct SQLContextBuilder {
+    runtime_env: Option<Arc<RuntimeEnv>>,
 }
 
-impl SQLContext {
-    /// Creates a new empty SQL context.
+impl SQLContextBuilder {
+    /// Creates a builder with DataFusion's default runtime environment.
     pub fn new() -> Self {
-        let state = SessionStateBuilder::new()
+        Self::default()
+    }
+
+    /// Uses the provided DataFusion runtime environment.
+    pub fn with_runtime_env(mut self, runtime_env: Arc<RuntimeEnv>) -> Self {
+        self.runtime_env = Some(runtime_env);
+        self
+    }
+
+    /// Builds a [`SQLContext`].
+    pub fn build(self) -> SQLContext {
+        let mut state_builder = SessionStateBuilder::new()
             .with_config(crate::lateral_vector_search::session_config())
             .with_default_features()
             .with_relation_planners(vec![Arc::new(
@@ -119,18 +154,39 @@ impl SQLContext {
             
.with_optimizer_rules(crate::lateral_vector_search::optimizer_rules())
             .with_query_planner(Arc::new(
                 crate::lateral_vector_search::PaimonQueryPlanner::new(),
-            ))
-            .build();
+            ));
+        if let Some(runtime_env) = self.runtime_env {
+            state_builder = state_builder.with_runtime_env(runtime_env);
+        }
+        let state = state_builder.build();
         let ctx = SessionContext::new_with_state(state);
         
crate::blob_descriptor_functions::register_blob_descriptor_functions(&ctx);
         crate::variant_functions::register_variant_functions(&ctx);
-        Self {
+        SQLContext {
             ctx,
             catalogs: HashMap::new(),
             dynamic_options: Default::default(),
             blob_reader_registry: BlobReaderRegistry::default(),
         }
     }
+}
+
+impl Default for SQLContext {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl SQLContext {
+    /// Creates a new empty SQL context.
+    pub fn new() -> Self {
+        Self::builder().build()
+    }
+
+    /// Creates a builder for customizing the underlying DataFusion session.
+    pub fn builder() -> SQLContextBuilder {
+        SQLContextBuilder::new()
+    }
 
     pub fn blob_reader_registry(&self) -> BlobReaderRegistry {
         self.blob_reader_registry.clone()
@@ -3601,6 +3657,8 @@ mod tests {
 
     use async_trait::async_trait;
     use datafusion::arrow::array::StringViewArray;
+    use datafusion::execution::memory_pool::FairSpillPool;
+    use datafusion::execution::runtime_env::RuntimeEnvBuilder;
     use datafusion::sql::sqlparser::dialect::PostgreSqlDialect;
     use paimon::catalog::Database;
     use paimon::spec::{
@@ -3608,6 +3666,21 @@ mod tests {
     };
     use paimon::table::Table;
 
+    #[test]
+    fn test_sql_context_builder_uses_custom_runtime_env() {
+        let runtime_env = RuntimeEnvBuilder::new()
+            .with_memory_pool(Arc::new(FairSpillPool::new(1024)))
+            .build_arc()
+            .unwrap();
+
+        let ctx = SQLContext::builder()
+            .with_runtime_env(Arc::clone(&runtime_env))
+            .build();
+
+        assert!(Arc::ptr_eq(&runtime_env, &ctx.ctx().runtime_env()));
+        assert_eq!("fair", ctx.ctx().runtime_env().memory_pool.name());
+    }
+
     // ==================== Mock Catalog ====================
 
     #[allow(clippy::enum_variant_names)]
diff --git a/docs/src/python-binding.md b/docs/src/python-binding.md
index 57dc657f..bc4aa3ee 100644
--- a/docs/src/python-binding.md
+++ b/docs/src/python-binding.md
@@ -94,6 +94,30 @@ for batch in batches:
     print(batch)
 ```
 
+### Runtime Resource Configuration
+
+`SQLContext` can use a bounded DataFusion memory pool and a dedicated temporary
+directory. This allows spill-capable operators, such as sorts and aggregations,
+to move intermediate data to disk when execution memory is constrained.
+
+```python
+ctx = SQLContext(
+    memory_pool_type="fair",
+    memory_pool_bytes=512 * 1024 * 1024,
+    temp_directory="/tmp/paimon-spill",
+    max_temp_directory_size_bytes=4 * 1024 * 1024 * 1024,
+)
+```
+
+`memory_pool_type` accepts `"fair"` or `"greedy"` and requires
+`memory_pool_bytes`. If only `memory_pool_bytes` is provided, the greedy pool 
is
+used. All arguments are optional, and `SQLContext()` continues to use
+DataFusion's default runtime environment.
+
+Memory limits apply to allocations tracked by DataFusion's memory pool. They do
+not account for every allocation made by the host application or by external
+libraries.
+
 ## Reading a Table
 
 Paimon Python uses a **scan-then-read** pattern: first scan the table to 
produce splits, then read data from those splits as PyArrow RecordBatches.

Reply via email to