Skip to content

Commit 9d166e3

Browse files
feat: add cloning support for SessionContext (#91)
1 parent 00e7189 commit 9d166e3

4 files changed

Lines changed: 99 additions & 6 deletions

File tree

native/src/context.rs

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use tokio_util::sync::CancellationToken;
66

77
use crate::{BytesData, Callback, ErrorCode, ErrorInfo, mappers, proto};
88

9+
#[derive(Clone)]
910
pub struct SessionContextWrapper {
1011
runtime: crate::RuntimeHandle,
1112
inner: Arc<datafusion::prelude::SessionContext>,
@@ -22,28 +23,30 @@ impl SessionContextWrapper {
2223

2324
/// Creates a new `SessionContext` bound to a runtime.
2425
///
26+
/// This is a synchronous operation.
27+
///
2528
/// # Safety
2629
/// - `runtime_ptr` must be a valid pointer returned by `datafusion_runtime_new`
27-
/// - `context_ptr` must be a valid, aligned, non-null pointer to writable memory
30+
/// - `context_out_ptr` must be a valid, aligned, non-null pointer to writable memory
2831
/// - Caller must call `datafusion_context_destroy` exactly once with the returned pointer
2932
#[unsafe(no_mangle)]
3033
pub unsafe extern "C" fn datafusion_context_new(
3134
runtime_ptr: *mut crate::RuntimeHandle,
32-
context_ptr: *mut *mut SessionContextWrapper,
35+
context_out_ptr: *mut *mut SessionContextWrapper,
3336
) -> ErrorCode {
34-
if context_ptr.is_null() {
37+
if context_out_ptr.is_null() {
3538
return ErrorCode::InvalidArgument;
3639
}
3740

3841
let runtime_handle = ffi_ref!(runtime_ptr);
3942

4043
let context = Box::new(SessionContextWrapper::new(Arc::clone(runtime_handle)));
41-
let raw_ptr = Box::into_raw(context);
44+
let context_ptr = Box::into_raw(context);
4245
unsafe {
43-
*context_ptr = raw_ptr;
46+
*context_out_ptr = context_ptr;
4447
}
4548

46-
debug!("Created session context {raw_ptr:p}");
49+
debug!("Created session context {context_ptr:p}");
4750

4851
ErrorCode::Ok
4952
}
@@ -66,6 +69,37 @@ pub unsafe extern "C" fn datafusion_context_destroy(
6669
ErrorCode::Ok
6770
}
6871

72+
/// Clones a `SessionContext`, creating a new wrapper that shares the same underlying context.
73+
///
74+
/// This is a synchronous operation.
75+
///
76+
/// # Safety
77+
/// - `context_ptr` must be a valid pointer returned by `datafusion_context_new` or `datafusion_context_clone`
78+
/// - `context_out_ptr` must be a valid, aligned, non-null pointer to writable memory
79+
/// - Caller must call `datafusion_context_destroy` exactly once with the returned pointer
80+
#[unsafe(no_mangle)]
81+
pub unsafe extern "C" fn datafusion_context_clone(
82+
context_ptr: *mut SessionContextWrapper,
83+
context_out_ptr: *mut *mut SessionContextWrapper,
84+
) -> ErrorCode {
85+
if context_out_ptr.is_null() {
86+
return ErrorCode::InvalidArgument;
87+
}
88+
89+
let context = ffi_ref!(context_ptr);
90+
91+
let cloned_context = Box::new(context.clone());
92+
let cloned_context_ptr = Box::into_raw(cloned_context);
93+
94+
unsafe {
95+
*context_out_ptr = cloned_context_ptr;
96+
}
97+
98+
debug!("Cloned session context {context_ptr:p} -> {cloned_context_ptr:p}");
99+
100+
ErrorCode::Ok
101+
}
102+
69103
/// Registers a CSV file as a table in the `SessionContext`.
70104
///
71105
/// This is an async operation. The callback is invoked on completion with no result data.

src/DataFusionSharp/Interop/NativeMethods.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ public static partial DataFusionErrorCode Ping(
6666
[LibraryImport(LibraryName, EntryPoint = "datafusion_context_destroy")]
6767
public static partial DataFusionErrorCode ContextDestroy(IntPtr contextHandle);
6868

69+
[LibraryImport(LibraryName, EntryPoint = "datafusion_context_clone")]
70+
public static partial DataFusionErrorCode ContextClone(SessionContextSafeHandle contextHandle, out IntPtr clonedContextHandle);
71+
6972
[LibraryImport(LibraryName, EntryPoint = "datafusion_context_register_csv")]
7073
public static partial DataFusionErrorCode ContextRegisterCsv(
7174
SessionContextSafeHandle contextHandle,

src/DataFusionSharp/SessionContext.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,22 @@ internal SessionContext(DataFusionRuntime runtime, SessionContextSafeHandle hand
3232
_handle = handle;
3333
}
3434

35+
/// <summary>
36+
/// Creates a clone of this session context.
37+
/// </summary>
38+
/// <remarks>
39+
/// The cloned context is an independent wrapper with its own lifecycle.
40+
/// </remarks>
41+
/// <returns>A cloned <see cref="SessionContext"/>.</returns>
42+
/// <exception cref="DataFusionException">Thrown when cloning fails.</exception>
43+
public SessionContext Clone()
44+
{
45+
var errorCode = NativeMethods.ContextClone(_handle, out var clonedContextHandle);
46+
DataFusionException.ThrowIfError(errorCode, "Failed to clone DataFusion context");
47+
48+
return new SessionContext(Runtime, new SessionContextSafeHandle(clonedContextHandle));
49+
}
50+
3551
/// <summary>
3652
/// Registers a CSV file as a table in this session.
3753
/// </summary>

tests/DataFusionSharp.Tests/SessionContextTests.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,46 @@ public async Task SqlAsync_WithMissingParameter_Throws()
173173
Assert.Contains("$value_b", ex.Message, StringComparison.Ordinal);
174174
Assert.DoesNotContain("$value_a", ex.Message, StringComparison.Ordinal);
175175
}
176+
177+
[Fact]
178+
public async Task Clone_RegisteredTableIsAvailableInClonedContext()
179+
{
180+
// Arrange
181+
using var context = _runtime.CreateSessionContext();
182+
await context.RegisterCsvAsync("customers", DataSet.CustomersCsvPath);
183+
184+
// Act
185+
using var clonedContext = context.Clone();
186+
using var clonedDataFrame = await clonedContext.SqlAsync("SELECT * FROM customers");
187+
var count = await clonedDataFrame.CountAsync();
188+
189+
// Assert
190+
Assert.NotSame(context, clonedContext);
191+
Assert.Same(context.Runtime, clonedContext.Runtime);
192+
Assert.Equal(10UL, count);
193+
}
194+
195+
[Fact]
196+
public async Task Clone_AfterOriginalDisposed_RemainsValid()
197+
{
198+
// Arrange
199+
SessionContext clonedContext;
200+
using (var context = _runtime.CreateSessionContext())
201+
{
202+
await context.RegisterCsvAsync("customers", DataSet.CustomersCsvPath);
203+
clonedContext = context.Clone();
204+
}
205+
206+
// Act
207+
using (clonedContext)
208+
{
209+
using var clonedDataFrame = await clonedContext.SqlAsync("SELECT * FROM customers");
210+
var count = await clonedDataFrame.CountAsync();
211+
212+
// Assert
213+
Assert.Equal(10UL, count);
214+
}
215+
}
176216

177217
public void Dispose()
178218
{

0 commit comments

Comments
 (0)