Copilot commented on code in PR #4983:
URL: https://github.com/apache/arrow-adbc/pull/4983#discussion_r4425886529


##########
csharp/src/Apache.Arrow.Adbc/C/CAdbcDriverExporter.Streams.cs:
##########
@@ -0,0 +1,353 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using System;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using Apache.Arrow.C;
+using Apache.Arrow.Ipc;
+
+namespace Apache.Arrow.Adbc.C
+{
+    public partial class CAdbcDriverExporter
+    {
+#if NET5_0_OR_GREATER
+        private static unsafe delegate* unmanaged<CArrowArrayStream*, 
CArrowSchema*, int> StreamGetSchemaPtr => &GetStreamSchema;
+        private static unsafe delegate* unmanaged<CArrowArrayStream*, 
CArrowArray*, int> StreamGetNextPtr => &GetStreamNext;
+        private static unsafe delegate* unmanaged<CArrowArrayStream*, byte*> 
StreamGetLastErrorPtr => &GetStreamLastError;
+        private static unsafe delegate* unmanaged<CArrowArrayStream*, void> 
StreamReleasePtr => &ReleaseStream;
+        private static unsafe delegate* unmanaged<CArrowArrayStream*, 
AdbcStatusCode*, CAdbcError*> ErrorFromArrayStreamPtr => 
&GetErrorFromArrayStream;
+#else
+        private static unsafe IntPtr StreamGetSchemaPtr = 
NativeDelegate<ArrayStreamGetSchema>.AsNativePointer(GetStreamSchema);
+        private static unsafe IntPtr StreamGetNextPtr = 
NativeDelegate<ArrayStreamGetNext>.AsNativePointer(GetStreamNext);
+        private static unsafe IntPtr StreamGetLastErrorPtr = 
NativeDelegate<ArrayStreamGetLastError>.AsNativePointer(GetStreamLastError);
+        private static unsafe IntPtr StreamReleasePtr = 
NativeDelegate<ArrayStreamRelease>.AsNativePointer(ReleaseStream);

Review Comment:
   On .NET Framework these pointers are backed by ADBC-specific delegate types, 
so an in-process Apache.Arrow consumer cannot invoke or release this stream 
using Arrow's own callback delegates: `Marshal.GetDelegateForFunctionPointer` 
returns the original delegate and the cast fails. The previous 
`CArrowArrayStreamExporter` produced callbacks that Arrow's importer and 
`CArrowArrayStream.Free` could consume. Please preserve interoperability with 
those consumers, not just with the new `ImportedArrayStream` wrapper.



##########
csharp/src/Apache.Arrow.Adbc/C/CAdbcDriverImporter.Streams.cs:
##########
@@ -0,0 +1,224 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Apache.Arrow.Adbc.Extensions;
+using Apache.Arrow.C;
+using Apache.Arrow.Ipc;
+
+namespace Apache.Arrow.Adbc.C
+{
+    public static partial class CAdbcDriverImporter
+    {
+        /// <summary>
+        /// A result stream imported from a driver. When the stream fails, it 
asks the driver
+        /// for the ADBC error behind the failure (see 
<c>AdbcErrorFromArrayStream</c>) and
+        /// throws it as an <see cref="AdbcException"/>.
+        /// </summary>
+        private sealed unsafe class ImportedArrayStream : IArrowArrayStream
+        {
+            private readonly ImportedAdbcDriver? _driver;
+            private readonly Schema _schema;
+            private CArrowArrayStream* _cStream;
+
+            /// <summary>
+            /// Imports <paramref name="cStream"/>, which must have been 
allocated with
+            /// <see cref="CArrowArrayStream.Create"/>. On success, the 
imported stream owns
+            /// it; on failure, the caller still does.
+            /// </summary>
+            public ImportedArrayStream(ImportedAdbcDriver? driver, 
CArrowArrayStream* cStream)
+            {
+                if (NativeArrayStream.From(cStream)->release == default)
+                {
+                    throw new ArgumentException("Tried to import an array 
stream that has already been released.", nameof(cStream));
+                }
+
+                _driver = driver;
+
+                CArrowSchema* cSchema = CArrowSchema.Create();
+                try
+                {
+                    int errno = GetSchema(cStream, cSchema);
+                    if (errno != 0)
+                    {
+                        throw StreamError(cStream, errno);
+                    }
+                    _schema = CArrowSchemaImporter.ImportSchema(cSchema);
+                }
+                finally
+                {
+                    CArrowSchema.Free(cSchema);
+                }
+
+                _cStream = cStream;
+            }
+
+            ~ImportedArrayStream()
+            {
+                Dispose();
+            }
+
+            public Schema Schema => _schema;
+
+            public ValueTask<RecordBatch?> 
ReadNextRecordBatchAsync(CancellationToken cancellationToken = default)
+            {
+                if (_cStream == null)
+                {
+                    throw new 
ObjectDisposedException(nameof(ImportedArrayStream));
+                }
+
+                if (cancellationToken.IsCancellationRequested)
+                {
+                    return new 
ValueTask<RecordBatch?>(Task.FromCanceled<RecordBatch?>(cancellationToken));
+                }
+
+                try
+                {
+                    return new ValueTask<RecordBatch?>(ReadNext());
+                }
+                catch (Exception e)
+                {
+                    return new 
ValueTask<RecordBatch?>(Task.FromException<RecordBatch?>(e));
+                }
+            }
+
+            private RecordBatch? ReadNext()
+            {
+                CArrowArray* cArray = CArrowArray.Create();
+                try
+                {
+                    int errno = GetNext(_cStream, cArray);
+                    if (errno != 0)
+                    {
+                        throw StreamError(_cStream, errno);
+                    }
+
+                    // A released array marks the end of the stream
+                    if (NativeArray.From(cArray)->release == IntPtr.Zero)
+                    {
+                        return null;
+                    }
+
+                    return CArrowArrayImporter.ImportRecordBatch(cArray, 
_schema);
+                }
+                finally
+                {
+                    CArrowArray.Free(cArray);
+                }
+            }
+
+            public void Dispose()
+            {
+                CArrowArrayStream* cStream = _cStream;
+                if (cStream != null)
+                {
+                    _cStream = null;
+                    FreeStream(cStream);
+                }
+                GC.SuppressFinalize(this);
+            }
+
+            /// <summary>
+            /// Releases and frees a stream allocated with <see 
cref="CArrowArrayStream.Create"/>.
+            /// </summary>
+            /// <remarks>
+            /// Unlike <see cref="CArrowArrayStream.Free"/>, this can release 
a stream that
+            /// <see cref="CAdbcDriverExporter"/> exported in the same process 
on .NET Framework,
+            /// where a callback created from a managed delegate can only be 
called through a
+            /// delegate of the same type.
+            /// </remarks>
+            public static void FreeStream(CArrowArrayStream* cStream)
+            {
+                NativeArrayStream* nativeStream = 
NativeArrayStream.From(cStream);
+                if (nativeStream->release != default)
+                {
+#if NET5_0_OR_GREATER
+                    nativeStream->release(cStream);
+#else
+                    
Marshal.GetDelegateForFunctionPointer<ArrayStreamRelease>(nativeStream->release)(cStream);
+#endif
+                    nativeStream->release = default;
+                }
+                CArrowArrayStream.Free(cStream);
+            }
+
+            /// <summary>
+            /// Builds the exception for a stream call that returned <paramref 
name="errno"/>:
+            /// the driver's ADBC error if it reports one, and otherwise the 
stream's last error.
+            /// </summary>
+            private Exception StreamError(CArrowArrayStream* cStream, int 
errno)
+            {
+                if (_driver != null && _driver.DriverVersion >= 
AdbcVersion.Version_1_1_0)
+                {
+                    ref CAdbcDriver nativeDriver = ref _driver.DriverUnsafe;
+                    if (nativeDriver.ErrorFromArrayStream != default)
+                    {
+                        AdbcStatusCode status = AdbcStatusCode.UnknownError;
+#if NET5_0_OR_GREATER
+                        CAdbcError* error = 
nativeDriver.ErrorFromArrayStream(cStream, &status);
+#else
+                        CAdbcError* error = 
Marshal.GetDelegateForFunctionPointer<ErrorFromArrayStream>(nativeDriver.ErrorFromArrayStream)(cStream,
 &status);
+#endif
+                        // The stream owns the error, so it must not be 
released here
+                        if (error != null)
+                        {
+                            IReadOnlyList<KeyValuePair<string, byte[]>>? 
details = ReadErrorDetails(_driver, error);
+                            return new ImportedAdbcException(
+                                ref *error,
+                                status == AdbcStatusCode.Success ? 
AdbcStatusCode.UnknownError : status,
+                                details);
+                        }
+                    }
+                }
+
+                NativeArrayStream* nativeStream = 
NativeArrayStream.From(cStream);
+#if NET5_0_OR_GREATER
+                byte* message = nativeStream->get_last_error(cStream);
+#else
+                byte* message = 
Marshal.GetDelegateForFunctionPointer<ArrayStreamGetLastError>(nativeStream->get_last_error)(cStream);
+#endif
+                return new Exception(message == null
+                    ? $"Array stream operation failed with no message. Error 
code: {errno}"
+                    : MarshalExtensions.PtrToStringUTF8(message));
+            }
+
+            private static int GetSchema(CArrowArrayStream* cStream, 
CArrowSchema* cSchema)
+            {
+                NativeArrayStream* nativeStream = 
NativeArrayStream.From(cStream);
+#if NET5_0_OR_GREATER
+                return nativeStream->get_schema(cStream, cSchema);
+#else
+                return 
Marshal.GetDelegateForFunctionPointer<ArrayStreamGetSchema>(nativeStream->get_schema)(cStream,
 cSchema);

Review Comment:
   On .NET Framework, importing an in-process `CArrowArrayStreamExporter` 
result throws `InvalidCastException`: its managed callback has Arrow's delegate 
type, not `ArrayStreamGetSchema`. The same mismatch affects `get_next`, 
`get_last_error`, and `FreeStream` at line 156, where cleanup of a rejected 
`BindStream` masks the driver error and leaves the stream unreleased. Use a 
callback bridge compatible with both Arrow-exported and ADBC-exported streams, 
including their release callbacks.



##########
csharp/src/Apache.Arrow.Adbc/C/CAdbcDriverExporter.Streams.cs:
##########
@@ -0,0 +1,353 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using System;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using Apache.Arrow.C;
+using Apache.Arrow.Ipc;
+
+namespace Apache.Arrow.Adbc.C
+{
+    public partial class CAdbcDriverExporter
+    {
+#if NET5_0_OR_GREATER
+        private static unsafe delegate* unmanaged<CArrowArrayStream*, 
CArrowSchema*, int> StreamGetSchemaPtr => &GetStreamSchema;
+        private static unsafe delegate* unmanaged<CArrowArrayStream*, 
CArrowArray*, int> StreamGetNextPtr => &GetStreamNext;
+        private static unsafe delegate* unmanaged<CArrowArrayStream*, byte*> 
StreamGetLastErrorPtr => &GetStreamLastError;
+        private static unsafe delegate* unmanaged<CArrowArrayStream*, void> 
StreamReleasePtr => &ReleaseStream;
+        private static unsafe delegate* unmanaged<CArrowArrayStream*, 
AdbcStatusCode*, CAdbcError*> ErrorFromArrayStreamPtr => 
&GetErrorFromArrayStream;
+#else
+        private static unsafe IntPtr StreamGetSchemaPtr = 
NativeDelegate<ArrayStreamGetSchema>.AsNativePointer(GetStreamSchema);
+        private static unsafe IntPtr StreamGetNextPtr = 
NativeDelegate<ArrayStreamGetNext>.AsNativePointer(GetStreamNext);
+        private static unsafe IntPtr StreamGetLastErrorPtr = 
NativeDelegate<ArrayStreamGetLastError>.AsNativePointer(GetStreamLastError);
+        private static unsafe IntPtr StreamReleasePtr = 
NativeDelegate<ArrayStreamRelease>.AsNativePointer(ReleaseStream);
+        private static unsafe IntPtr ErrorFromArrayStreamPtr = 
NativeDelegate<ErrorFromArrayStream>.AsNativePointer(GetErrorFromArrayStream);
+#endif
+
+        /// <summary>
+        /// Exports a stream produced by the managed driver. The exported 
stream takes
+        /// ownership on success; if exporting fails, nobody else will dispose 
it.
+        /// </summary>
+        /// <remarks>
+        /// When reading the stream fails, the exported stream keeps the 
exception so that
+        /// <c>AdbcErrorFromArrayStream</c> can report its status, SQLSTATE, 
vendor code and
+        /// details, and returns an errno that corresponds to the status.
+        /// </remarks>
+        private unsafe static void ExportStream(IArrowArrayStream? stream, 
CArrowArrayStream* cStream)
+        {
+            try
+            {
+                if (stream == null)
+                {
+                    throw new ArgumentNullException(nameof(stream));
+                }
+                if (cStream == null)
+                {
+                    throw new ArgumentNullException(nameof(cStream));
+                }
+
+                NativeArrayStream* nativeStream = 
NativeArrayStream.From(cStream);
+                nativeStream->private_data = 
(void*)GCHandle.ToIntPtr(GCHandle.Alloc(new ExportedStream(stream)));
+                nativeStream->get_schema = StreamGetSchemaPtr;
+                nativeStream->get_next = StreamGetNextPtr;
+                nativeStream->get_last_error = StreamGetLastErrorPtr;
+                nativeStream->release = StreamReleasePtr;
+            }
+            catch
+            {
+                stream?.Dispose();
+                throw;
+            }
+        }
+
+        private unsafe static ExportedStream? 
GetExportedStream(CArrowArrayStream* cStream)
+        {
+            NativeArrayStream* nativeStream = NativeArrayStream.From(cStream);
+            if (cStream == null ||
+                (IntPtr)nativeStream->release != (IntPtr)StreamReleasePtr ||
+                nativeStream->private_data == null)
+            {
+                return null;
+            }
+            return 
GCHandle.FromIntPtr((IntPtr)nativeStream->private_data).Target as 
ExportedStream;
+        }
+
+#if NET5_0_OR_GREATER
+        [UnmanagedCallersOnly]
+#endif
+        private unsafe static int GetStreamSchema(CArrowArrayStream* cStream, 
CArrowSchema* cSchema)
+        {
+            ExportedStream? stream = null;
+            try
+            {
+                stream = GetExportedStream(cStream) ?? throw new 
ArgumentException("not a stream exported by this driver", nameof(cStream));
+                stream.ClearError();
+                CArrowSchemaExporter.ExportSchema(stream.Stream.Schema, 
cSchema);
+                return 0;
+            }
+            catch (Exception e)
+            {
+                return stream?.SetError(e) ?? Errno.EINVAL;
+            }
+        }
+
+#if NET5_0_OR_GREATER
+        [UnmanagedCallersOnly]
+#endif
+        private unsafe static int GetStreamNext(CArrowArrayStream* cStream, 
CArrowArray* cArray)
+        {
+            ExportedStream? stream = null;
+            try
+            {
+                NativeArray.From(cArray)->release = default;
+                stream = GetExportedStream(cStream) ?? throw new 
ArgumentException("not a stream exported by this driver", nameof(cStream));
+                stream.ClearError();
+
+                ValueTask<RecordBatch?> pending = 
stream.Stream.ReadNextRecordBatchAsync();
+                RecordBatch? batch = pending.IsCompletedSuccessfully ? 
pending.Result : pending.AsTask().GetAwaiter().GetResult();

Review Comment:
   Starting the asynchronous read on the caller's synchronization context and 
then blocking that thread can deadlock an in-process read on a UI or other 
single-threaded context. `DatabricksCompositeReader` awaits without suppressing 
context capture, so its continuation cannot run while `get_next` waits here. 
Start the read on a context-free worker before synchronously waiting, following 
the pattern in `AdbcStatement11.ExecuteQuery`.



##########
csharp/src/Apache.Arrow.Adbc/C/CAdbcDriverExporter.Streams.cs:
##########
@@ -0,0 +1,353 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using System;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using Apache.Arrow.C;
+using Apache.Arrow.Ipc;
+
+namespace Apache.Arrow.Adbc.C
+{
+    public partial class CAdbcDriverExporter
+    {
+#if NET5_0_OR_GREATER
+        private static unsafe delegate* unmanaged<CArrowArrayStream*, 
CArrowSchema*, int> StreamGetSchemaPtr => &GetStreamSchema;
+        private static unsafe delegate* unmanaged<CArrowArrayStream*, 
CArrowArray*, int> StreamGetNextPtr => &GetStreamNext;
+        private static unsafe delegate* unmanaged<CArrowArrayStream*, byte*> 
StreamGetLastErrorPtr => &GetStreamLastError;
+        private static unsafe delegate* unmanaged<CArrowArrayStream*, void> 
StreamReleasePtr => &ReleaseStream;
+        private static unsafe delegate* unmanaged<CArrowArrayStream*, 
AdbcStatusCode*, CAdbcError*> ErrorFromArrayStreamPtr => 
&GetErrorFromArrayStream;
+#else
+        private static unsafe IntPtr StreamGetSchemaPtr = 
NativeDelegate<ArrayStreamGetSchema>.AsNativePointer(GetStreamSchema);
+        private static unsafe IntPtr StreamGetNextPtr = 
NativeDelegate<ArrayStreamGetNext>.AsNativePointer(GetStreamNext);
+        private static unsafe IntPtr StreamGetLastErrorPtr = 
NativeDelegate<ArrayStreamGetLastError>.AsNativePointer(GetStreamLastError);
+        private static unsafe IntPtr StreamReleasePtr = 
NativeDelegate<ArrayStreamRelease>.AsNativePointer(ReleaseStream);
+        private static unsafe IntPtr ErrorFromArrayStreamPtr = 
NativeDelegate<ErrorFromArrayStream>.AsNativePointer(GetErrorFromArrayStream);
+#endif
+
+        /// <summary>
+        /// Exports a stream produced by the managed driver. The exported 
stream takes
+        /// ownership on success; if exporting fails, nobody else will dispose 
it.
+        /// </summary>
+        /// <remarks>
+        /// When reading the stream fails, the exported stream keeps the 
exception so that
+        /// <c>AdbcErrorFromArrayStream</c> can report its status, SQLSTATE, 
vendor code and
+        /// details, and returns an errno that corresponds to the status.
+        /// </remarks>
+        private unsafe static void ExportStream(IArrowArrayStream? stream, 
CArrowArrayStream* cStream)
+        {
+            try
+            {
+                if (stream == null)
+                {
+                    throw new ArgumentNullException(nameof(stream));
+                }
+                if (cStream == null)
+                {
+                    throw new ArgumentNullException(nameof(cStream));
+                }
+
+                NativeArrayStream* nativeStream = 
NativeArrayStream.From(cStream);
+                nativeStream->private_data = 
(void*)GCHandle.ToIntPtr(GCHandle.Alloc(new ExportedStream(stream)));
+                nativeStream->get_schema = StreamGetSchemaPtr;
+                nativeStream->get_next = StreamGetNextPtr;
+                nativeStream->get_last_error = StreamGetLastErrorPtr;
+                nativeStream->release = StreamReleasePtr;
+            }
+            catch
+            {
+                stream?.Dispose();
+                throw;
+            }
+        }
+
+        private unsafe static ExportedStream? 
GetExportedStream(CArrowArrayStream* cStream)
+        {
+            NativeArrayStream* nativeStream = NativeArrayStream.From(cStream);
+            if (cStream == null ||
+                (IntPtr)nativeStream->release != (IntPtr)StreamReleasePtr ||
+                nativeStream->private_data == null)
+            {
+                return null;
+            }
+            return 
GCHandle.FromIntPtr((IntPtr)nativeStream->private_data).Target as 
ExportedStream;
+        }
+
+#if NET5_0_OR_GREATER
+        [UnmanagedCallersOnly]
+#endif
+        private unsafe static int GetStreamSchema(CArrowArrayStream* cStream, 
CArrowSchema* cSchema)
+        {
+            ExportedStream? stream = null;
+            try
+            {
+                stream = GetExportedStream(cStream) ?? throw new 
ArgumentException("not a stream exported by this driver", nameof(cStream));
+                stream.ClearError();
+                CArrowSchemaExporter.ExportSchema(stream.Stream.Schema, 
cSchema);
+                return 0;
+            }
+            catch (Exception e)
+            {
+                return stream?.SetError(e) ?? Errno.EINVAL;
+            }
+        }
+
+#if NET5_0_OR_GREATER
+        [UnmanagedCallersOnly]
+#endif
+        private unsafe static int GetStreamNext(CArrowArrayStream* cStream, 
CArrowArray* cArray)
+        {
+            ExportedStream? stream = null;
+            try
+            {
+                NativeArray.From(cArray)->release = default;
+                stream = GetExportedStream(cStream) ?? throw new 
ArgumentException("not a stream exported by this driver", nameof(cStream));
+                stream.ClearError();
+
+                ValueTask<RecordBatch?> pending = 
stream.Stream.ReadNextRecordBatchAsync();
+                RecordBatch? batch = pending.IsCompletedSuccessfully ? 
pending.Result : pending.AsTask().GetAwaiter().GetResult();
+                if (batch != null)
+                {
+                    CArrowArrayExporter.ExportRecordBatch(batch, cArray);
+                }
+                return 0;
+            }
+            catch (Exception e)
+            {
+                return stream?.SetError(e) ?? Errno.EINVAL;
+            }
+        }
+
+#if NET5_0_OR_GREATER
+        [UnmanagedCallersOnly]
+#endif
+        private unsafe static byte* GetStreamLastError(CArrowArrayStream* 
cStream)
+        {
+            try
+            {
+                return GetExportedStream(cStream)?.LastErrorMessage;
+            }
+            catch (Exception)
+            {
+                return null;
+            }
+        }
+
+#if NET5_0_OR_GREATER
+        [UnmanagedCallersOnly]
+#endif
+        private unsafe static void ReleaseStream(CArrowArrayStream* cStream)
+        {
+            NativeArrayStream* nativeStream = NativeArrayStream.From(cStream);
+            try
+            {
+                if (nativeStream->private_data != null)
+                {
+                    GCHandle gch = 
GCHandle.FromIntPtr((IntPtr)nativeStream->private_data);
+                    try
+                    {
+                        ((ExportedStream)gch.Target!).Dispose();
+                    }
+                    finally
+                    {
+                        gch.Free();
+                        nativeStream->private_data = null;
+                    }
+                }
+            }
+            catch (Exception)
+            {
+                // An exception would end the process, and the consumer has no 
way to learn of it.
+            }
+            nativeStream->release = default;
+        }
+
+#if NET5_0_OR_GREATER
+        [UnmanagedCallersOnly]
+#endif
+        private unsafe static CAdbcError* 
GetErrorFromArrayStream(CArrowArrayStream* cStream, AdbcStatusCode* status)
+        {
+            try
+            {
+                return GetExportedStream(cStream)?.GetError(status);
+            }
+            catch (Exception)
+            {
+                return null;
+            }
+        }
+
+        /// <summary>
+        /// The state behind an exported <see cref="CArrowArrayStream"/>: the 
managed stream
+        /// and the error from the last call that failed, if any.
+        /// </summary>
+        private sealed unsafe class ExportedStream : IDisposable
+        {
+            private IArrowArrayStream? _stream;
+            private CAdbcError* _error;
+            private AdbcStatusCode _status;
+
+            public ExportedStream(IArrowArrayStream stream)
+            {
+                _stream = stream;
+            }
+
+            public IArrowArrayStream Stream => _stream ?? throw new 
ObjectDisposedException(nameof(ExportedStream));
+
+            public byte* LastErrorMessage => _error != null && _error->release 
!= default ? _error->message : null;
+
+            public CAdbcError* GetError(AdbcStatusCode* status)
+            {
+                if (_error == null || _error->release == default)
+                {
+                    return null;
+                }
+                if (status != null)
+                {
+                    *status = _status;
+                }
+                return _error;
+            }
+
+            /// <summary>
+            /// Records <paramref name="exception"/> as the stream's error and 
returns the
+            /// errno that corresponds to its status.
+            /// </summary>
+            public int SetError(Exception exception)
+            {
+                ClearError();
+                _status = CAdbcDriverExporter.SetError(null, exception);
+                if (_status == AdbcStatusCode.Success)
+                {
+                    // An errno of 0 would read as the end of the stream
+                    _status = AdbcStatusCode.UnknownError;
+                }
+
+                try
+                {
+                    if (_error == null)
+                    {
+                        _error = 
(CAdbcError*)Marshal.AllocHGlobal(sizeof(CAdbcError));
+                        *_error = default;
+                    }
+
+                    // AdbcErrorFromArrayStream is part of ADBC 1.1.0, so the 
consumer
+                    // understands the 1.1.0 error layout.
+                    _error->vendor_code = ErrorVendorCodePrivateData;
+                    CAdbcDriverExporter.SetError(_error, exception);

Review Comment:
   A stream exception with both a nonzero `NativeError` and `Details` does not 
preserve the vendor code as promised. `FillError` uses the vendor-code field 
for the private-data marker when details are present, and 
`ImportedAdbcException` consequently reports `NativeError` as 0. Please define 
a way to carry and recover both values, or document the ABI limitation, and add 
a round-trip case that supplies both.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to