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


##########
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:
   Fixed in 2e2cae80e. On .NET Framework, exported stream callbacks are now 
created with Apache.Arrow's own callback delegate types, so an in-process 
Apache.Arrow importer (and `CArrowArrayStream.Free`) can call them. Covered by 
`ArrowImporterReadsExportedStream`, which fails on net472 without the fix.



##########
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:
   Fixed in 2e2cae80e. This risk predates the PR (Arrow's 
`CArrowArrayStreamExporter` also blocks on 
`ReadNextRecordBatchAsync().Result`), but when `get_next` is called on a thread 
with a synchronization context or non-default scheduler, the read is now 
started on a thread-pool thread. Native callers have no context and keep the 
direct path. Covered by `ExportedStreamDoesNotBlockSynchronizationContext`.



##########
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:
   Fixed in 2e2cae80e. The importer now calls `get_schema`, `get_next`, 
`get_last_error` and `release` through Apache.Arrow's delegate types on .NET 
Framework, so it works for streams exported in-process by either Apache.Arrow 
or `CAdbcDriverExporter`, including releasing a stream the driver didn't take 
in `BindStream`. Covered by `RejectedBindStreamIsReleased`.



##########
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:
   This is a limitation of the ADBC C ABI rather than of this change: when an 
error carries details, `vendor_code` must be 
`ADBC_ERROR_VENDOR_CODE_PRIVATE_DATA` so the consumer knows `private_data` is 
valid, which leaves no field for the driver's own vendor code. `FillError` 
already handles every exported error this way, so stream errors are now 
consistent with errors returned directly from ADBC functions. Leaving as is.



-- 
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