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-adbc.git


The following commit(s) were added to refs/heads/main by this push:
     new 9ee52cd2f feat(csharp/src/Client): use the cancellable statement APIs 
in AdbcCommand (#4901)
9ee52cd2f is described below

commit 9ee52cd2fe3420e9050b3e2629a28883abec8636
Author: Curt Hagenlocher <[email protected]>
AuthorDate: Thu Oct 15 13:07:08 2026 -0700

    feat(csharp/src/Client): use the cancellable statement APIs in AdbcCommand 
(#4901)
    
    ## What's changed
    
    The ADO.NET wrapper's `AdbcCommand` now uses the asynchronous,
    cancellable statement methods added in #4895:
    
    - **`ExecuteReaderAsync`** passes its `CancellationToken` to
    `AdbcStatement.ExecuteQueryAsync(CancellationToken)`. Before, the token
    was checked once up front and then dropped.
    - **`ExecuteNonQueryAsync`** is now overridden to call
    `AdbcStatement.ExecuteUpdateAsync(CancellationToken)`. The `DbCommand`
    default it replaces runs `ExecuteNonQuery` synchronously on the caller's
    thread and ignores the token once the command has started.
    - **`Cancel`** now calls `AdbcStatement.Cancel` instead of throwing
    `NotImplementedException`. As the `DbCommand.Cancel` contract requires,
    it does nothing if there's nothing to cancel, and it doesn't throw if
    the driver can't cancel.
    
    This is the fourth of several PRs implementing ADBC 1.1. #4884 fixed
    bugs in the C importer and exporter, #4885 added the 1.1 APIs to the
    base classes, and #4895 added async methods and cancellation. Wider
    DuckDB tests come next.
    
    ### Compatibility
    
    - **Drivers that implement only the synchronous methods** keep working
    through the base class defaults, which run them on the thread pool.
    - **Drivers that override the older parameterless `ExecuteQueryAsync()`
    or `ExecuteUpdateAsync()`** still have those overrides called, because
    the token overloads forward to them by default.
    - **`Cancel` no longer throws `NotImplementedException`.** Code that
    relied on that exception would see a change, but `DbCommand.Cancel`
    isn't supposed to throw.
    - **`ExecuteScalarAsync` and `PrepareAsync`** still use the `DbCommand`
    defaults. `ExecuteScalar` isn't implemented yet, so they're left for a
    follow-up.
    
    ## Testing
    
    New and updated tests in `ClientTests`:
    
    - **Token passing:** `ExecuteReaderAsync` and `ExecuteNonQueryAsync`
    pass the caller's token to the statement.
    - **Sync fallback:** `ExecuteNonQueryAsync` works with a driver that
    implements only `ExecuteUpdate`.
    - **Pre-cancelled tokens:** a token that's already cancelled stops
    `ExecuteNonQueryAsync` before it reaches the statement.
    - **`Cancel`:** it forwards to the statement, and it doesn't throw when
    the driver can't cancel.
    - **Updated mocks:** the existing tests now set up the token overload of
    `ExecuteQueryAsync`, which the command calls.
    
    **Results:**
    - The full solution builds without warnings, and `dotnet format
    --verify-no-changes` is clean.
    - The core test project passes on net10.0, net8.0 and net472.
    - 10 full runs with `DOTNET_PROCESSOR_COUNT=2` had no failures.
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 csharp/src/Client/AdbcCommand.cs                   | 35 ++++++++--
 .../Apache.Arrow.Adbc.Tests/Client/ClientTests.cs  | 81 +++++++++++++++++++++-
 2 files changed, 107 insertions(+), 9 deletions(-)

diff --git a/csharp/src/Client/AdbcCommand.cs b/csharp/src/Client/AdbcCommand.cs
index e4eb7fd29..d11812f2c 100644
--- a/csharp/src/Client/AdbcCommand.cs
+++ b/csharp/src/Client/AdbcCommand.cs
@@ -182,6 +182,15 @@ namespace Apache.Arrow.Adbc.Client
             return Convert.ToInt32(AdbcStatement.ExecuteUpdate().AffectedRows);
         }
 
+        public override async Task<int> ExecuteNonQueryAsync(CancellationToken 
cancellationToken)
+        {
+            cancellationToken.ThrowIfCancellationRequested();
+
+            BindParameters();
+            UpdateResult result = await 
AdbcStatement.ExecuteUpdateAsync(cancellationToken).ConfigureAwait(false);
+            return Convert.ToInt32(result.AffectedRows);
+        }
+
         /// <summary>
         /// Similar to <see cref="ExecuteNonQuery"/> but returns Int64
         /// instead of Int32.
@@ -217,7 +226,7 @@ namespace Apache.Arrow.Adbc.Client
             cancellationToken.ThrowIfCancellationRequested();
 
             BindParameters();
-            QueryResult result = await 
AdbcStatement.ExecuteQueryAsync().ConfigureAwait(false);
+            QueryResult result = await 
AdbcStatement.ExecuteQueryAsync(cancellationToken).ConfigureAwait(false);
 
             return new AdbcDataReader(this, result, this.DecimalBehavior, 
this.StructBehavior, closeConnection);
         }
@@ -561,6 +570,25 @@ namespace Apache.Arrow.Adbc.Client
         }
 #endif
 
+        /// <summary>
+        /// Attempts to cancel the execution of this command.
+        /// </summary>
+        /// <remarks>
+        /// As <see cref="DbCommand.Cancel"/> requires, nothing happens if 
there is nothing
+        /// to cancel, and no exception is thrown if the attempt to cancel 
fails, for
+        /// example because the driver doesn't support cancellation.
+        /// </remarks>
+        public override void Cancel()
+        {
+            try
+            {
+                _adbcStatement.Cancel();
+            }
+            catch (Exception)
+            {
+            }
+        }
+
         #region NOT_IMPLEMENTED
 
         public override bool DesignTimeVisible { get => throw new 
NotImplementedException(); set => throw new NotImplementedException(); }
@@ -569,11 +597,6 @@ namespace Apache.Arrow.Adbc.Client
 
         protected override DbTransaction? DbTransaction { get => throw new 
NotImplementedException(); set => throw new NotImplementedException(); }
 
-        public override void Cancel()
-        {
-            throw new NotImplementedException();
-        }
-
         public override object ExecuteScalar()
         {
             throw new NotImplementedException();
diff --git a/csharp/test/Apache.Arrow.Adbc.Tests/Client/ClientTests.cs 
b/csharp/test/Apache.Arrow.Adbc.Tests/Client/ClientTests.cs
index 7c96dd02e..a95d25d43 100644
--- a/csharp/test/Apache.Arrow.Adbc.Tests/Client/ClientTests.cs
+++ b/csharp/test/Apache.Arrow.Adbc.Tests/Client/ClientTests.cs
@@ -294,10 +294,85 @@ namespace Apache.Arrow.Adbc.Tests.Client
             {
             }
 
-            fixture.Statement.Verify(x => x.ExecuteQueryAsync(), Times.Once);
+            fixture.Statement.Verify(x => 
x.ExecuteQueryAsync(It.IsAny<CancellationToken>()), Times.Once);
             fixture.Statement.Verify(x => x.ExecuteQuery(), Times.Never);
         }
 
+        [Fact]
+        public async Task 
ExecuteReaderAsyncPassesTheCancellationTokenToTheStatement()
+        {
+            AsyncReaderFixture fixture = AsyncReaderFixture.Create(batchCount: 
1, rowsPerBatch: 1);
+
+            using CancellationTokenSource cts = new CancellationTokenSource();
+            using (DbDataReader reader = await 
fixture.Command.ExecuteReaderAsync(cts.Token))
+            {
+            }
+
+            fixture.Statement.Verify(x => x.ExecuteQueryAsync(cts.Token), 
Times.Once);
+        }
+
+        [Fact]
+        public async Task ExecuteNonQueryAsyncUsesTheAsyncStatementPath()
+        {
+            Mock<AdbcStatement> mockStatement = new Mock<AdbcStatement>();
+            mockStatement
+                .Setup(x => 
x.ExecuteUpdateAsync(It.IsAny<CancellationToken>()))
+                .Returns(Task.FromResult(new UpdateResult(7)));
+
+            AdbcCommand command = new AdbcCommand(mockStatement.Object, new 
AdbcClient.AdbcConnection());
+
+            using CancellationTokenSource cts = new CancellationTokenSource();
+            Assert.Equal(7, await command.ExecuteNonQueryAsync(cts.Token));
+
+            mockStatement.Verify(x => x.ExecuteUpdateAsync(cts.Token), 
Times.Once);
+            mockStatement.Verify(x => x.ExecuteUpdate(), Times.Never);
+        }
+
+        [Fact]
+        public async Task ExecuteNonQueryAsyncFallsBackToSyncExecuteUpdate()
+        {
+            Mock<AdbcStatement> mockStatement = new Mock<AdbcStatement>() { 
CallBase = true };
+            mockStatement.Setup(x => x.ExecuteUpdate()).Returns(new 
UpdateResult(3));
+
+            AdbcCommand command = new AdbcCommand(mockStatement.Object, new 
AdbcClient.AdbcConnection());
+
+            Assert.Equal(3, await command.ExecuteNonQueryAsync());
+        }
+
+        [Fact]
+        public void CancelForwardsToTheStatement()
+        {
+            Mock<AdbcStatement> mockStatement = new Mock<AdbcStatement>();
+            AdbcCommand command = new AdbcCommand(mockStatement.Object, new 
AdbcClient.AdbcConnection());
+
+            command.Cancel();
+
+            mockStatement.Verify(x => x.Cancel(), Times.Once);
+        }
+
+        [Fact]
+        public void CancelDoesNotThrowWhenTheDriverCannotCancel()
+        {
+            // The base AdbcStatement.Cancel throws NotImplemented
+            Mock<AdbcStatement> mockStatement = new Mock<AdbcStatement>() { 
CallBase = true };
+            AdbcCommand command = new AdbcCommand(mockStatement.Object, new 
AdbcClient.AdbcConnection());
+
+            command.Cancel();
+        }
+
+        [Fact]
+        public async Task ExecuteNonQueryAsyncHonorsAPreCancelledToken()
+        {
+            Mock<AdbcStatement> mockStatement = new Mock<AdbcStatement>();
+            AdbcCommand command = new AdbcCommand(mockStatement.Object, new 
AdbcClient.AdbcConnection());
+
+            using CancellationTokenSource cts = new CancellationTokenSource();
+            cts.Cancel();
+
+            await Assert.ThrowsAnyAsync<OperationCanceledException>(() => 
command.ExecuteNonQueryAsync(cts.Token));
+            mockStatement.Verify(x => 
x.ExecuteUpdateAsync(It.IsAny<CancellationToken>()), Times.Never);
+        }
+
         [Fact]
         public async Task ReadAsyncPassesTheCancellationTokenToTheStream()
         {
@@ -568,7 +643,7 @@ namespace Apache.Arrow.Adbc.Tests.Client
 
         /// <param name="syncOnlyDriver">
         /// Stub <see cref="AdbcStatement.ExecuteQuery"/> only, leaving the 
base
-        /// <see cref="AdbcStatement.ExecuteQueryAsync"/> to supply the result.
+        /// <see cref="AdbcStatement.ExecuteQueryAsync(CancellationToken)"/> 
to supply the result.
         /// </param>
         public static AsyncReaderFixture Create(
             int batchCount,
@@ -606,7 +681,7 @@ namespace Apache.Arrow.Adbc.Tests.Client
             }
             else
             {
-                mockStatement.Setup(x => x.ExecuteQueryAsync()).Returns(new 
ValueTask<QueryResult>(queryResult));
+                mockStatement.Setup(x => 
x.ExecuteQueryAsync(It.IsAny<CancellationToken>())).Returns(new 
ValueTask<QueryResult>(queryResult));
             }
 
             AdbcClient.AdbcConnection connection = new 
AdbcClient.AdbcConnection();

Reply via email to