-
-
Notifications
You must be signed in to change notification settings - Fork 91
Add Apache Arrow result streaming to DuckDBCommand #334
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Giorgi
merged 3 commits into
Giorgi:develop
from
luisquintanilla:feat-arrow-result-stream
Jun 24, 2026
Merged
Changes from 2 commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| namespace DuckDB.NET.Native; | ||
|
|
||
| public partial class NativeMethods | ||
| { | ||
| //https://duckdb.org/docs/stable/clients/c/api#arrow-interface | ||
| public static partial class Arrow | ||
| { | ||
| [LibraryImport(DuckDbLibrary, EntryPoint = "duckdb_result_get_arrow_options")] | ||
| [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] | ||
| public static partial DuckDBArrowOptions DuckDBResultGetArrowOptions(ref DuckDBResult result); | ||
|
|
||
| [LibraryImport(DuckDbLibrary, EntryPoint = "duckdb_destroy_arrow_options")] | ||
| [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] | ||
| public static partial void DuckDBDestroyArrowOptions(ref IntPtr arrowOptions); | ||
|
|
||
| // duckdb_error_data duckdb_to_arrow_schema(duckdb_arrow_options, duckdb_logical_type *types, | ||
| // const char **names, idx_t column_count, ArrowSchema *out_schema) | ||
| [LibraryImport(DuckDbLibrary, EntryPoint = "duckdb_to_arrow_schema")] | ||
| [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] | ||
| public static partial IntPtr DuckDBToArrowSchema(DuckDBArrowOptions arrowOptions, IntPtr types, IntPtr names, ulong columnCount, IntPtr outSchema); | ||
|
|
||
| // duckdb_error_data duckdb_data_chunk_to_arrow(duckdb_arrow_options, duckdb_data_chunk, ArrowArray *out_arrow_array) | ||
| [LibraryImport(DuckDbLibrary, EntryPoint = "duckdb_data_chunk_to_arrow")] | ||
| [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] | ||
| public static partial IntPtr DuckDBDataChunkToArrow(DuckDBArrowOptions arrowOptions, DuckDBDataChunk chunk, IntPtr outArray); | ||
|
|
||
| [SuppressGCTransition] | ||
| [LibraryImport(DuckDbLibrary, EntryPoint = "duckdb_error_data_has_error")] | ||
| [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] | ||
| [return: MarshalAs(UnmanagedType.I1)] | ||
| public static partial bool DuckDBErrorDataHasError(IntPtr errorData); | ||
|
|
||
| [LibraryImport(DuckDbLibrary, EntryPoint = "duckdb_error_data_message")] | ||
| [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] | ||
| [return: MarshalUsing(typeof(DuckDBOwnedStringMarshaller))] | ||
| public static partial string DuckDBErrorDataMessage(IntPtr errorData); | ||
|
|
||
| [LibraryImport(DuckDbLibrary, EntryPoint = "duckdb_destroy_error_data")] | ||
| [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] | ||
| public static partial void DuckDBDestroyErrorData(ref IntPtr errorData); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,184 @@ | ||
| using System.Runtime.InteropServices; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
| using Apache.Arrow; | ||
| using Apache.Arrow.C; | ||
| using Apache.Arrow.Ipc; | ||
| using DuckDB.NET.Native; | ||
|
|
||
| namespace DuckDB.NET.Data.Arrow; | ||
|
|
||
| /// <summary> | ||
| /// Streams the rows of a DuckDB query result as Apache Arrow <see cref="RecordBatch"/> values using | ||
| /// DuckDB's Arrow C Data Interface (<c>duckdb_to_arrow_schema</c> / <c>duckdb_data_chunk_to_arrow</c>). | ||
| /// Each DuckDB data chunk is converted into one Arrow record batch and imported with no row-by-row marshaling. | ||
| /// </summary> | ||
| public sealed class DuckDBArrowArrayStream : IArrowArrayStream | ||
|
Giorgi marked this conversation as resolved.
Outdated
|
||
| { | ||
| private DuckDBResult result; | ||
| private readonly DuckDBArrowOptions arrowOptions; | ||
| private readonly bool streaming; | ||
| private bool disposed; | ||
|
|
||
| public Schema Schema { get; } | ||
|
|
||
| internal DuckDBArrowArrayStream(DuckDBResult result) | ||
| { | ||
| this.result = result; | ||
|
|
||
| arrowOptions = NativeMethods.Arrow.DuckDBResultGetArrowOptions(ref this.result); | ||
| if (arrowOptions.IsInvalid) | ||
| { | ||
| this.result.Close(); | ||
| throw new InvalidOperationException("Failed to obtain Arrow options from the DuckDB result."); | ||
| } | ||
|
|
||
| streaming = NativeMethods.Types.DuckDBResultIsStreaming(this.result) > 0; | ||
|
|
||
| try | ||
| { | ||
| Schema = BuildSchema(); | ||
| } | ||
| catch | ||
| { | ||
| arrowOptions.Dispose(); | ||
| this.result.Close(); | ||
| throw; | ||
| } | ||
| } | ||
|
Giorgi marked this conversation as resolved.
|
||
|
|
||
| private unsafe Schema BuildSchema() | ||
| { | ||
| var columnCount = NativeMethods.Query.DuckDBColumnCount(ref result); | ||
|
|
||
| var logicalTypes = new DuckDBLogicalType[columnCount]; | ||
| var typeHandles = new IntPtr[columnCount]; | ||
| var namePointers = new IntPtr[columnCount]; | ||
|
|
||
| try | ||
| { | ||
| for (var index = 0UL; index < columnCount; index++) | ||
| { | ||
| var logicalType = NativeMethods.Query.DuckDBColumnLogicalType(ref result, (long)index); | ||
| logicalTypes[index] = logicalType; | ||
| typeHandles[index] = logicalType.DangerousGetHandle(); | ||
|
|
||
| var name = NativeMethods.Query.DuckDBColumnName(ref result, (long)index); | ||
| namePointers[index] = Marshal.StringToCoTaskMemUTF8(name); | ||
| } | ||
|
Giorgi marked this conversation as resolved.
|
||
|
|
||
| var cSchema = CArrowSchema.Create(); | ||
|
|
||
| try | ||
| { | ||
| fixed (IntPtr* typesPointer = typeHandles) | ||
| fixed (IntPtr* namesPointer = namePointers) | ||
| { | ||
| var error = NativeMethods.Arrow.DuckDBToArrowSchema(arrowOptions, (IntPtr)typesPointer, (IntPtr)namesPointer, columnCount, (IntPtr)cSchema); | ||
| ThrowIfError(error, "Failed to convert the DuckDB result schema to an Arrow schema."); | ||
| } | ||
|
|
||
| return CArrowSchemaImporter.ImportSchema(cSchema); | ||
| } | ||
| finally | ||
| { | ||
| CArrowSchema.Free(cSchema); | ||
| } | ||
| } | ||
| finally | ||
| { | ||
| foreach (var pointer in namePointers) | ||
| { | ||
| if (pointer != IntPtr.Zero) | ||
| { | ||
| Marshal.FreeCoTaskMem(pointer); | ||
| } | ||
| } | ||
|
|
||
| foreach (var logicalType in logicalTypes) | ||
| { | ||
| logicalType?.Dispose(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| public ValueTask<RecordBatch?> ReadNextRecordBatchAsync(CancellationToken cancellationToken = default) | ||
| { | ||
| ObjectDisposedException.ThrowIf(disposed, this); | ||
|
|
||
| if (cancellationToken.IsCancellationRequested) | ||
| { | ||
| return new ValueTask<RecordBatch?>(Task.FromCanceled<RecordBatch?>(cancellationToken)); | ||
| } | ||
|
|
||
| var chunk = streaming | ||
| ? NativeMethods.StreamingResult.DuckDBStreamFetchChunk(result) | ||
| : NativeMethods.Query.DuckDBFetchChunk(result); | ||
|
|
||
| if (chunk.IsInvalid) | ||
| { | ||
| chunk.Dispose(); | ||
| return new ValueTask<RecordBatch?>((RecordBatch?)null); | ||
| } | ||
|
|
||
| try | ||
| { | ||
| return new ValueTask<RecordBatch?>(ConvertChunk(chunk)); | ||
| } | ||
| finally | ||
| { | ||
| chunk.Dispose(); | ||
| } | ||
| } | ||
|
|
||
| private unsafe RecordBatch ConvertChunk(DuckDBDataChunk chunk) | ||
| { | ||
| var cArray = CArrowArray.Create(); | ||
|
|
||
| try | ||
| { | ||
| var error = NativeMethods.Arrow.DuckDBDataChunkToArrow(arrowOptions, chunk, (IntPtr)cArray); | ||
| ThrowIfError(error, "Failed to convert a DuckDB data chunk to an Arrow array."); | ||
|
|
||
| return CArrowArrayImporter.ImportRecordBatch(cArray, Schema); | ||
| } | ||
| finally | ||
| { | ||
| CArrowArray.Free(cArray); | ||
| } | ||
| } | ||
|
|
||
| private static void ThrowIfError(IntPtr errorData, string message) | ||
|
Giorgi marked this conversation as resolved.
Outdated
|
||
| { | ||
| if (errorData == IntPtr.Zero) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| try | ||
| { | ||
| if (NativeMethods.Arrow.DuckDBErrorDataHasError(errorData)) | ||
| { | ||
| var detail = NativeMethods.Arrow.DuckDBErrorDataMessage(errorData); | ||
| throw new InvalidOperationException($"{message} {detail}".TrimEnd()); | ||
|
Giorgi marked this conversation as resolved.
Outdated
|
||
| } | ||
| } | ||
| finally | ||
| { | ||
| NativeMethods.Arrow.DuckDBDestroyErrorData(ref errorData); | ||
| } | ||
| } | ||
|
|
||
| public void Dispose() | ||
| { | ||
| if (disposed) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| disposed = true; | ||
|
|
||
| arrowOptions.Dispose(); | ||
| result.Close(); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.