-
-
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 all 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
Some comments aren't visible on the classic Files Changed page.
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,27 @@ | ||
| 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 DuckDBErrorData 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 DuckDBErrorData DuckDBDataChunkToArrow(DuckDBArrowOptions arrowOptions, DuckDBDataChunk chunk, IntPtr outArray); | ||
| } | ||
| } |
23 changes: 23 additions & 0 deletions
23
DuckDB.NET.Bindings/NativeMethods/NativeMethods.ErrorData.cs
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,23 @@ | ||
| namespace DuckDB.NET.Native; | ||
|
|
||
| public partial class NativeMethods | ||
| { | ||
| //https://duckdb.org/docs/stable/clients/c/api#error-data | ||
| public static partial class ErrorData | ||
| { | ||
| [SuppressGCTransition] | ||
| [LibraryImport(DuckDbLibrary, EntryPoint = "duckdb_error_data_has_error")] | ||
| [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] | ||
| [return: MarshalAs(UnmanagedType.I1)] | ||
| public static partial bool DuckDBErrorDataHasError(DuckDBErrorData errorData); | ||
|
|
||
| [LibraryImport(DuckDbLibrary, EntryPoint = "duckdb_error_data_message")] | ||
| [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] | ||
| [return: MarshalUsing(typeof(DuckDBOwnedStringMarshaller))] | ||
| public static partial string DuckDBErrorDataMessage(DuckDBErrorData 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,163 @@ | ||
| 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> | ||
| internal sealed class DuckDBArrowArrayStream : IArrowArrayStream | ||
| { | ||
| 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; | ||
| } | ||
| } | ||
|
|
||
| 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); | ||
| } | ||
|
|
||
| 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); | ||
| error.ThrowOnError("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); | ||
| error.ThrowOnError("Failed to convert a DuckDB data chunk to an Arrow array."); | ||
|
|
||
| return CArrowArrayImporter.ImportRecordBatch(cArray, Schema); | ||
| } | ||
| finally | ||
| { | ||
| CArrowArray.Free(cArray); | ||
| } | ||
| } | ||
|
|
||
| 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
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,15 @@ | ||
| namespace DuckDB.NET.Data.Extensions; | ||
|
|
||
| internal static class DuckDBErrorDataExtensions | ||
| { | ||
| public static void ThrowOnError(this DuckDBErrorData errorData, string message) | ||
| { | ||
| using (errorData) | ||
| { | ||
| if (errorData.HasError) | ||
| { | ||
| throw new DuckDBException($"{message} {errorData.Message}".TrimEnd()); | ||
| } | ||
| } | ||
| } | ||
| } |
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.