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

CurtHagenlocher pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-dotnet.git


The following commit(s) were added to refs/heads/main by this push:
     new 2e54e4e  feat: add ICompressionCodecFactory support to Flight record 
batch readers (#285)
2e54e4e is described below

commit 2e54e4ee4d51eec0083714f700c281884ec10cd4
Author: David Linton <[email protected]>
AuthorDate: Mon Jul 6 21:54:41 2026 +0200

    feat: add ICompressionCodecFactory support to Flight record batch readers 
(#285)
    
    ## Summary
    
    Resolves #184.
    
    ### The Problem
    
    When a Flight server sends IPC-compressed record batches (LZ4/Zstd), the
    C# Flight client throws:
    
    > "Body is compressed with codec LZ4_FRAME but no
    ICompressionCodecFactory has been configured to decompress buffers"
    
    The decompression infrastructure already exists — `ArrowStreamReader`
    accepts `ICompressionCodecFactory` and the `Apache.Arrow.Compression`
    package provides LZ4/Zstd codecs. But the Flight reader chain never
    passes the factory through:
    
    ```
    FlightClient.GetStream()
      → FlightClientRecordBatchStreamReader(stream)
        → FlightRecordBatchStreamReader(stream)
          → RecordBatchReaderImplementation(stream)
            → ArrowReaderImplementation()          ← parameterless, 
_compressionCodecFactory = null
    ```
    
    `RecordBatchReaderImplementation` calls the base constructor without a
    codec factory, so `GetBufferCreator()` always hits the "no factory
    configured" error path.
    
    ### The Fix
    
    Thread `ICompressionCodecFactory` through the reader hierarchy (5 files,
    all with `= null` defaults for backward compatibility):
    
    1. **`RecordBatchReaderImplementation`** — accept
    `ICompressionCodecFactory`, pass to `base(allocator: null,
    compressionCodecFactory)`
    2. **`FlightRecordBatchStreamReader`** — accept and forward to
    `RecordBatchReaderImplementation`
    3. **`FlightClientRecordBatchStreamReader`** — forward to base
    4. **`FlightServerRecordBatchStreamReader`** — forward to base (both
    constructors)
    5. **`FlightClient`** — store as field, add constructor overloads, pass
    to readers in `GetStream()` and `DoExchange()`
    
    After the fix:
    
    ```
    FlightClient(channel, new CompressionCodecFactory())
      → stores _compressionCodecFactory
    
    FlightClient.GetStream(ticket)
      → FlightClientRecordBatchStreamReader(stream, _compressionCodecFactory)
        → FlightRecordBatchStreamReader(stream, compressionCodecFactory)
          → RecordBatchReaderImplementation(stream, compressionCodecFactory)
            → ArrowReaderImplementation(null, compressionCodecFactory)  ← 
decompression works
    ```
    
    ### Usage
    
    ```csharp
    using Apache.Arrow.Compression;
    
    // Before (compressed batches cause error):
    var client = new FlightClient(channel);
    
    // After (LZ4/Zstd decompression handled transparently):
    var client = new FlightClient(channel, new CompressionCodecFactory());
    ```
    
    ### Impact
    
    - **25 lines added, 8 removed** across 5 files
    - **No breaking changes** — all new parameters default to `null`
    - **No new dependencies** — `ICompressionCodecFactory` is in
    `Apache.Arrow.Ipc`, already referenced by the Flight package
    
    ## Test plan
    
    - [ ] Existing Flight tests pass unchanged (backward compatibility via
    `null` defaults)
    - [ ] Test with a Flight server sending LZ4-compressed record batches
    - [ ] Test with a Flight server sending Zstd-compressed record batches
    - [ ] Confirm `GetStream()` and `DoExchange()` correctly decompress when
    `ICompressionCodecFactory` is provided
    - [ ] Confirm no regression when `ICompressionCodecFactory` is `null`
    (existing behavior)
---
 src/Apache.Arrow.Flight/Client/FlightClient.cs     |  31 +++++-
 .../Client/FlightClientRecordBatchStreamReader.cs  |   2 +-
 .../FlightRecordBatchStreamReader.cs               |   4 +-
 .../Internal/RecordBatchReaderImplementation.cs    |   3 +-
 .../Server/FlightServerRecordBatchStreamReader.cs  |   9 +-
 test/Apache.Arrow.Flight.Tests/FlightTests.cs      | 105 +++++++++++++++++++++
 6 files changed, 146 insertions(+), 8 deletions(-)

diff --git a/src/Apache.Arrow.Flight/Client/FlightClient.cs 
b/src/Apache.Arrow.Flight/Client/FlightClient.cs
index d094266..c8dc82f 100644
--- a/src/Apache.Arrow.Flight/Client/FlightClient.cs
+++ b/src/Apache.Arrow.Flight/Client/FlightClient.cs
@@ -27,15 +27,42 @@ namespace Apache.Arrow.Flight.Client
         internal static readonly Empty EmptyInstance = new Empty();
 
         private readonly FlightService.FlightServiceClient _client;
+        private readonly ArrowContext _context;
 
         public FlightClient(ChannelBase grpcChannel)
+            : this(grpcChannel, null)
+        {
+        }
+
+        public FlightClient(ChannelBase grpcChannel, ArrowContext context)
         {
             _client = new FlightService.FlightServiceClient(grpcChannel);
+            _context = context;
         }
 
         public FlightClient(CallInvoker callInvoker)
+            : this(callInvoker, null)
+        {
+        }
+
+        private FlightClient(CallInvoker callInvoker, ArrowContext context)
         {
             _client = new FlightService.FlightServiceClient(callInvoker);
+            _context = context;
+        }
+
+        /// <summary>
+        /// Creates a <see cref="FlightClient"/> from a <see 
cref="CallInvoker"/> with an <see cref="ArrowContext"/>.
+        /// </summary>
+        /// <remarks>
+        /// A factory method is used instead of a constructor overload so that 
dependency injection
+        /// (e.g. Grpc.Net.ClientFactory) can still resolve the 
single-argument constructor unambiguously.
+        /// </remarks>
+        /// <param name="callInvoker">The <see cref="CallInvoker"/> to use for 
gRPC calls</param>
+        /// <param name="context">The <see cref="ArrowContext"/> carrying the 
compression codec factory and other reader configuration</param>
+        public static FlightClient Create(CallInvoker callInvoker, 
ArrowContext context)
+        {
+            return new FlightClient(callInvoker, context);
         }
 
         public AsyncServerStreamingCall<FlightInfo> ListFlights(FlightCriteria 
criteria = null, Metadata headers = null)
@@ -77,7 +104,7 @@ namespace Apache.Arrow.Flight.Client
         public FlightRecordBatchStreamingCall GetStream(FlightTicket ticket, 
Metadata headers, System.DateTime? deadline, CancellationToken 
cancellationToken = default)
         {
             var stream = _client.DoGet(ticket.ToProtocol(), headers, deadline, 
cancellationToken);
-            var responseStream = new 
FlightClientRecordBatchStreamReader(stream.ResponseStream);
+            var responseStream = new 
FlightClientRecordBatchStreamReader(stream.ResponseStream, _context);
             return new FlightRecordBatchStreamingCall(responseStream, 
stream.ResponseHeadersAsync, stream.GetStatus, stream.GetTrailers, 
stream.Dispose);
         }
 
@@ -207,7 +234,7 @@ namespace Apache.Arrow.Flight.Client
         {
             var channel = _client.DoExchange(headers, deadline, 
cancellationToken);
             var requestStream = new 
FlightClientRecordBatchStreamWriter(channel.RequestStream, flightDescriptor);
-            var responseStream = new 
FlightClientRecordBatchStreamReader(channel.ResponseStream);
+            var responseStream = new 
FlightClientRecordBatchStreamReader(channel.ResponseStream, _context);
             var call = new FlightRecordBatchExchangeCall(
                 requestStream,
                 responseStream,
diff --git 
a/src/Apache.Arrow.Flight/Client/FlightClientRecordBatchStreamReader.cs 
b/src/Apache.Arrow.Flight/Client/FlightClientRecordBatchStreamReader.cs
index 65eb6b6..fc0bd89 100644
--- a/src/Apache.Arrow.Flight/Client/FlightClientRecordBatchStreamReader.cs
+++ b/src/Apache.Arrow.Flight/Client/FlightClientRecordBatchStreamReader.cs
@@ -21,7 +21,7 @@ namespace Apache.Arrow.Flight.Client
 {
     public class FlightClientRecordBatchStreamReader : 
FlightRecordBatchStreamReader
     {
-        internal 
FlightClientRecordBatchStreamReader(IAsyncStreamReader<Protocol.FlightData> 
flightDataStream) : base(flightDataStream)
+        internal 
FlightClientRecordBatchStreamReader(IAsyncStreamReader<Protocol.FlightData> 
flightDataStream, ArrowContext context = null) : base(flightDataStream, context)
         {
         }
     }
diff --git a/src/Apache.Arrow.Flight/FlightRecordBatchStreamReader.cs 
b/src/Apache.Arrow.Flight/FlightRecordBatchStreamReader.cs
index 7400ec1..7336b85 100644
--- a/src/Apache.Arrow.Flight/FlightRecordBatchStreamReader.cs
+++ b/src/Apache.Arrow.Flight/FlightRecordBatchStreamReader.cs
@@ -40,9 +40,9 @@ namespace Apache.Arrow.Flight
 
         private readonly RecordBatchReaderImplementation 
_arrowReaderImplementation;
 
-        private protected 
FlightRecordBatchStreamReader(IAsyncStreamReader<Protocol.FlightData> 
flightDataStream)
+        private protected 
FlightRecordBatchStreamReader(IAsyncStreamReader<Protocol.FlightData> 
flightDataStream, ArrowContext context = null)
         {
-            _arrowReaderImplementation = new 
RecordBatchReaderImplementation(flightDataStream);
+            _arrowReaderImplementation = new 
RecordBatchReaderImplementation(flightDataStream, context);
         }
 
         public ValueTask<Schema> Schema => 
_arrowReaderImplementation.GetSchemaAsync();
diff --git 
a/src/Apache.Arrow.Flight/Internal/RecordBatchReaderImplementation.cs 
b/src/Apache.Arrow.Flight/Internal/RecordBatchReaderImplementation.cs
index 16e080d..54fd8c2 100644
--- a/src/Apache.Arrow.Flight/Internal/RecordBatchReaderImplementation.cs
+++ b/src/Apache.Arrow.Flight/Internal/RecordBatchReaderImplementation.cs
@@ -32,7 +32,8 @@ namespace Apache.Arrow.Flight.Internal
         private FlightDescriptor _flightDescriptor;
         private readonly List<ByteString> _applicationMetadatas;
 
-        public 
RecordBatchReaderImplementation(IAsyncStreamReader<Protocol.FlightData> 
streamReader)
+        public 
RecordBatchReaderImplementation(IAsyncStreamReader<Protocol.FlightData> 
streamReader, ArrowContext context = null)
+            : base(context?.Allocator, context?.CompressionCodecFactory, 
context?.ExtensionRegistry)
         {
             _flightDataStream = streamReader;
             _applicationMetadatas = new List<ByteString>();
diff --git 
a/src/Apache.Arrow.Flight/Server/FlightServerRecordBatchStreamReader.cs 
b/src/Apache.Arrow.Flight/Server/FlightServerRecordBatchStreamReader.cs
index c52b761..ca93610 100644
--- a/src/Apache.Arrow.Flight/Server/FlightServerRecordBatchStreamReader.cs
+++ b/src/Apache.Arrow.Flight/Server/FlightServerRecordBatchStreamReader.cs
@@ -21,11 +21,16 @@ namespace Apache.Arrow.Flight.Server
 {
     public class FlightServerRecordBatchStreamReader : 
FlightRecordBatchStreamReader
     {
-        public 
FlightServerRecordBatchStreamReader(IAsyncStreamReader<FlightData> 
flightDataStream) : base(new StreamReader<FlightData, 
Protocol.FlightData>(flightDataStream, data => data.ToProtocol()))
+        public 
FlightServerRecordBatchStreamReader(IAsyncStreamReader<FlightData> 
flightDataStream)
+            : this(flightDataStream, null)
         {
         }
 
-        internal 
FlightServerRecordBatchStreamReader(IAsyncStreamReader<Protocol.FlightData> 
flightDataStream) : base(flightDataStream)
+        public 
FlightServerRecordBatchStreamReader(IAsyncStreamReader<FlightData> 
flightDataStream, ArrowContext context) : base(new StreamReader<FlightData, 
Protocol.FlightData>(flightDataStream, data => data.ToProtocol()), context)
+        {
+        }
+
+        internal 
FlightServerRecordBatchStreamReader(IAsyncStreamReader<Protocol.FlightData> 
flightDataStream, ArrowContext context = null) : base(flightDataStream, context)
         {
         }
 
diff --git a/test/Apache.Arrow.Flight.Tests/FlightTests.cs 
b/test/Apache.Arrow.Flight.Tests/FlightTests.cs
index 8c69211..46662cc 100644
--- a/test/Apache.Arrow.Flight.Tests/FlightTests.cs
+++ b/test/Apache.Arrow.Flight.Tests/FlightTests.cs
@@ -577,5 +577,110 @@ namespace Apache.Arrow.Flight.Tests
 
             SchemaComparer.Compare(expectedSchema, actualSchema);
         }
+
+        [Fact]
+        public async Task 
TestIntegrationWithGrpcNetClientFactoryAndArrowContext()
+        {
+            IServiceCollection services = new ServiceCollection();
+
+            services.AddGrpcClient<FlightClient>(grpc => grpc.Address = new 
Uri(_testWebFactory.GetAddress()))
+                .ConfigureGrpcClientCreator(invoker =>
+                {
+                    return FlightClient.Create(invoker, new ArrowContext());
+                });
+
+            IServiceProvider provider = services.BuildServiceProvider();
+
+            // Test that a FlightClient carrying an ArrowContext can be 
created via the
+            // Grpc.Net.ClientFactory library using the FlightClient.Create 
factory method.
+            FlightClient flightClient = 
provider.GetRequiredService<FlightClient>();
+
+            // Test that the resolved client is functional.
+            var flightDescriptor = 
FlightDescriptor.CreatePathDescriptor("test-factory-context");
+            var expectedBatch = CreateTestBatch(0, 100);
+            var expectedSchema = expectedBatch.Schema;
+
+            GivenStoreBatches(flightDescriptor, new 
RecordBatchWithMetadata(expectedBatch));
+
+            var actualSchema = await flightClient.GetSchema(flightDescriptor);
+
+            SchemaComparer.Compare(expectedSchema, actualSchema);
+        }
+
+        [Fact]
+        public async Task TestGetWithArrowContext()
+        {
+            // Verify that FlightClient works when constructed with an 
ArrowContext
+            var context = new ArrowContext();
+            var flightClient = new FlightClient(_testWebFactory.GetChannel(), 
context);
+
+            var flightDescriptor = 
FlightDescriptor.CreatePathDescriptor("test-context");
+            var expectedBatch = CreateTestBatch(0, 100);
+
+            GivenStoreBatches(flightDescriptor, new 
RecordBatchWithMetadata(expectedBatch));
+
+            var flightInfo = await flightClient.GetInfo(flightDescriptor);
+            var endpoint = flightInfo.Endpoints.FirstOrDefault();
+
+            var getStream = flightClient.GetStream(endpoint.Ticket);
+            var resultList = await getStream.ResponseStream.ToListAsync();
+
+            Assert.Single(resultList);
+            ArrowReaderVerifier.CompareBatches(expectedBatch, resultList[0]);
+        }
+
+        [Fact]
+        public async Task TestGetWithNullArrowContext()
+        {
+            // Verify that FlightClient works with null ArrowContext (backward 
compat)
+            var flightClient = new FlightClient(_testWebFactory.GetChannel(), 
null);
+
+            var flightDescriptor = 
FlightDescriptor.CreatePathDescriptor("test-null-context");
+            var expectedBatch = CreateTestBatch(0, 100);
+
+            GivenStoreBatches(flightDescriptor, new 
RecordBatchWithMetadata(expectedBatch));
+
+            var flightInfo = await flightClient.GetInfo(flightDescriptor);
+            var endpoint = flightInfo.Endpoints.FirstOrDefault();
+
+            var getStream = flightClient.GetStream(endpoint.Ticket);
+            var resultList = await getStream.ResponseStream.ToListAsync();
+
+            Assert.Single(resultList);
+            ArrowReaderVerifier.CompareBatches(expectedBatch, resultList[0]);
+        }
+
+        [Fact]
+        public async Task TestPutAndGetWithArrowContext()
+        {
+            // Verify put + get round-trip works with ArrowContext
+            var context = new ArrowContext();
+            var flightClient = new FlightClient(_testWebFactory.GetChannel(), 
context);
+
+            var flightDescriptor = 
FlightDescriptor.CreatePathDescriptor("test-context-roundtrip");
+            var expectedBatch = CreateTestBatch(0, 100);
+
+            var putStream = await flightClient.StartPut(flightDescriptor, 
expectedBatch.Schema);
+            await putStream.RequestStream.WriteAsync(expectedBatch);
+            await putStream.RequestStream.CompleteAsync();
+            await putStream.ResponseStream.ToListAsync();
+
+            var flightInfo = await flightClient.GetInfo(flightDescriptor);
+            var endpoint = flightInfo.Endpoints.FirstOrDefault();
+
+            var getStream = flightClient.GetStream(endpoint.Ticket);
+            var resultList = await getStream.ResponseStream.ToListAsync();
+
+            Assert.Single(resultList);
+            ArrowReaderVerifier.CompareBatches(expectedBatch, resultList[0]);
+        }
+
+        [Fact]
+        public void TestFlightClientDefaultConstructorStillWorks()
+        {
+            // Verify the original constructor without ArrowContext still works
+            var client = new FlightClient(_testWebFactory.GetChannel());
+            Assert.NotNull(client);
+        }
     }
 }

Reply via email to