Copilot commented on code in PR #4885:
URL: https://github.com/apache/arrow-adbc/pull/4885#discussion_r4260903530
##########
csharp/src/Apache.Arrow.Adbc/C/CAdbcDriverImporter.cs:
##########
@@ -362,25 +370,47 @@ public unsafe override AdbcDatabase
Open(IReadOnlyDictionary<string, object> par
{
if (parameters == null) throw new
ArgumentNullException(nameof(parameters));
+ if (_version < AdbcVersion.Version_1_1_0)
+ {
+ return Open(AdbcOptionValue.ToOptionStrings(parameters));
+ }
Review Comment:
For a negotiated 1.0 driver, converting the whole dictionary with
`ToOptionStrings` rejects binary values as `InvalidArgument`. The base `Open`
fallback instead opens with the string options and then invokes
`SetOptionBytes`, which produces the documented `NotImplemented` result and
disposes the database on failure. Delegate to the base implementation here to
preserve those fallback semantics.
##########
csharp/src/Apache.Arrow.Adbc/AdbcDriver.cs:
##########
@@ -40,11 +44,48 @@ public abstract class AdbcDriver : IDisposable
/// Open a database via this driver.
/// </summary>
/// <param name="parameters">
- /// Driver-specific parameters.
+ /// Driver-specific parameters. Values may be strings, booleans,
integers,
+ /// floating-point numbers, byte arrays or <see
cref="ReadOnlyMemory{T}"/> of bytes.
/// </param>
+ /// <remarks>
+ /// The default implementation converts the non-binary values to
strings and calls
+ /// <see cref="Open(IReadOnlyDictionary{string, string})"/>, then
applies binary values
+ /// to the new database through <see
cref="AdbcDatabase.SetOptionBytes"/>. Drivers that
+ /// need binary options while opening should override this method.
+ /// </remarks>
public virtual AdbcDatabase Open(IReadOnlyDictionary<string, object>
parameters)
{
- throw AdbcException.NotImplemented("Driver does not support
non-string parameters");
+ if (parameters == null) throw new
ArgumentNullException(nameof(parameters));
+
+ Dictionary<string, string> stringParameters = new
Dictionary<string, string>(parameters.Count);
+ List<KeyValuePair<string, ReadOnlyMemory<byte>>> binaryParameters
= new List<KeyValuePair<string, ReadOnlyMemory<byte>>>();
+ foreach (KeyValuePair<string, object> pair in parameters)
+ {
+ if (AdbcOptionValue.TryGetBytes(pair.Value, out
ReadOnlyMemory<byte> bytes))
+ {
+ binaryParameters.Add(new KeyValuePair<string,
ReadOnlyMemory<byte>>(pair.Key, bytes));
+ }
+ else
+ {
+ stringParameters[pair.Key] =
AdbcOptionValue.ToOptionString(pair.Key, pair.Value);
+ }
+ }
+
+ AdbcDatabase database = Open(stringParameters);
+ try
+ {
+ foreach (KeyValuePair<string, ReadOnlyMemory<byte>> pair in
binaryParameters)
+ {
+ database.SetOptionBytes(pair.Key, pair.Value.Span);
Review Comment:
The new default binary-option path is not exercised:
`DriverOpenWithObjectsConvertsToStrings` only supplies scalar values, while the
round-trip fixture overrides this object overload entirely. Add base-class
tests that verify binary values reach `SetOptionBytes` and that the opened
database is disposed if applying a binary option fails, matching the coverage
already present for `ConnectWithTypedOptions`.
##########
csharp/src/Apache.Arrow.Adbc/C/CAdbcDriverExporter.cs:
##########
@@ -19,14 +19,30 @@
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
+using System.Text;
using Apache.Arrow.Adbc.Extensions;
using Apache.Arrow.C;
using Apache.Arrow.Ipc;
namespace Apache.Arrow.Adbc.C
{
+ /// <summary>
+ /// Exposes a managed <see cref="AdbcDriver"/> through the ADBC C API.
+ /// </summary>
+ /// <remarks>
+ /// When a caller requests ADBC 1.1.0, every 1.1.0 entry point is
populated.
+ /// Functionality that the managed driver doesn't override surfaces as
+ /// <see cref="AdbcStatusCode.NotImplemented"/>, which is the same status a
+ /// driver manager reports for a missing entry point.
Review Comment:
The public remarks say every ADBC 1.1 entry point is populated, but
`ErrorFromArrayStream` is explicitly left null below. Narrow this statement so
consumers are not promised an entry point the exporter does not provide.
##########
csharp/test/Apache.Arrow.Adbc.Tests/ExportedDriverRoundTrip11Tests.cs:
##########
@@ -0,0 +1,650 @@
+/*
+ * 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.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Apache.Arrow.Adbc.C;
+using Apache.Arrow.Ipc;
+using Apache.Arrow.Types;
+using Xunit;
+
+namespace Apache.Arrow.Adbc.Tests
+{
+ /// <summary>
+ /// Round-trips the ADBC 1.1.0 API surface through <see
cref="CAdbcDriverExporter"/>
+ /// and <see cref="CAdbcDriverImporter"/>: typed options, statistics,
error details
+ /// and the standard connection options.
+ /// </summary>
+ public class ExportedDriverRoundTrip11Tests
+ {
+ [Fact]
+ public void TypedOpenParametersAreMarshaledToProducer()
+ {
+ var fixture = new Driver11();
+ using AdbcDriver imported = Load(fixture);
+
+ using AdbcDatabase db = imported.Open(new Dictionary<string,
object>
+ {
+ { "string", "value" },
+ { "int", 42L },
+ { "double", 2.5 },
+ { "bytes", new byte[] { 1, 2, 3 } },
+ { "bool", true },
+ });
+
+ Dictionary<string, object> options = fixture.LastDatabase!.Options;
+ Assert.Equal("value", options["string"]);
+ Assert.Equal(42L, options["int"]);
+ Assert.Equal(2.5, options["double"]);
+ Assert.Equal(new byte[] { 1, 2, 3 }, options["bytes"]);
+ Assert.Equal("true", options["bool"]);
+ }
+
+ [Fact]
+ public void DatabaseTypedOptionsRoundTrip()
+ {
+ var fixture = new Driver11();
+ using AdbcDriver imported = Load(fixture);
+ using AdbcDatabase db = imported.Open(new Dictionary<string,
string>());
+
+ db.SetOption("s", "text");
+ db.SetOptionInt64("i", long.MinValue);
+ db.SetOptionDouble("d", -0.125);
+ db.SetOptionBytes("b", new byte[] { 0, 255 });
+
+ Assert.Equal("text", db.GetOption("s"));
+ Assert.Equal(long.MinValue, db.GetOptionInt64("i"));
+ Assert.Equal(-0.125, db.GetOptionDouble("d"));
+ Assert.Equal(new byte[] { 0, 255 }, db.GetOptionBytes("b"));
+ }
+
+ [Fact]
+ public void DatabaseGetOptionReportsOptionsTheProducerDoesNotTrack()
+ {
+ // A 1.0-style producer that never overrode GetOption still answers
+ // DatabaseGetOption for options set through the C API.
+ var fixture = new Driver11 { DatabaseTracksOptions = false };
+ using AdbcDriver imported = Load(fixture);
+ using AdbcDatabase db = imported.Open(new Dictionary<string,
string> { { "uri", "memory://" } });
+
+ Assert.Equal("memory://", db.GetOption("uri"));
+ AdbcException ex = Assert.ThrowsAny<AdbcException>(() =>
db.GetOption("missing"));
+ Assert.Equal(AdbcStatusCode.NotFound, ex.Status);
+ }
+
+ [Fact]
+ public void LongStringOptionUsesBufferRetry()
+ {
+ var fixture = new Driver11();
+ using AdbcDriver imported = Load(fixture);
+ using AdbcDatabase db = imported.Open(new Dictionary<string,
string>());
+ using AdbcConnection conn = db.Connect(null);
+ using AdbcStatement stmt = conn.CreateStatement();
+
+ string longValue = new string('x', 1000) + "é";
+ stmt.SetOption("long", longValue);
+ Assert.Equal(longValue, stmt.GetOption("long"));
+
+ byte[] longBytes = new byte[1000];
+ new Random(1).NextBytes(longBytes);
+ stmt.SetOptionBytes("long_bytes", longBytes);
+ Assert.Equal(longBytes, stmt.GetOptionBytes("long_bytes"));
+ }
+
+ [Fact]
+ public void ConnectionTypedOptionsRoundTrip()
+ {
+ var fixture = new Driver11();
+ using AdbcDriver imported = Load(fixture);
+ using AdbcDatabase db = imported.Open(new Dictionary<string,
string>());
+ using AdbcConnection conn = db.Connect(new Dictionary<string,
string> { { "at_connect", "yes" } });
+
+ conn.SetOptionInt64("timeout", 30);
+ conn.SetOptionDouble("ratio", 0.75);
+
+ Assert.Equal(30L, fixture.LastConnection!.Options["timeout"]);
+ Assert.Equal(0.75, fixture.LastConnection!.Options["ratio"]);
+ Assert.Equal(30L, conn.GetOptionInt64("timeout"));
+ Assert.Equal(0.75, conn.GetOptionDouble("ratio"));
+ Assert.Equal("yes", conn.GetOption("at_connect"));
+ }
+
+ [Fact]
+ public void StandardConnectionOptionsComeFromProperties()
+ {
+ var fixture = new Driver11();
+ using AdbcDriver imported = Load(fixture);
+ using AdbcDatabase db = imported.Open(new Dictionary<string,
string>());
+ using AdbcConnection conn = db.Connect(null);
+
+ // The producer doesn't override GetOption for these keys; the base
+ // AdbcConnection implementation answers from its properties.
+ Assert.Equal(AdbcOptions.Enabled,
conn.GetOption(AdbcOptions.Connection.Autocommit));
+ Assert.True(conn.AutoCommit);
+ Assert.Equal(IsolationLevel.Default, conn.IsolationLevel);
+
+ conn.CurrentCatalog = "main";
+ Assert.Equal("main", fixture.LastConnection!.CurrentCatalog);
+ Assert.Equal("main", conn.CurrentCatalog);
+ Assert.Equal("main",
conn.GetOption(AdbcOptions.Connection.CurrentCatalog));
+
+ // The producer doesn't support a current schema at all
+ Assert.Null(conn.CurrentDbSchema);
+ }
+
+ [Fact]
+ public void TypedConnectOptionsReachTheProducerBeforeInit()
+ {
+ var fixture = new Driver11 { DatabaseHandlesTypedConnect = true };
+ using AdbcDriver imported = Load(fixture);
+ using AdbcDatabase db = imported.Open(new Dictionary<string,
string>());
+ using AdbcConnection conn = db.ConnectWithTypedOptions(new
Dictionary<string, object>
+ {
+ { "s", "text" },
+ { "n", 5L },
+ { "d", 0.5 },
+ { "cert", new byte[] { 1, 2, 3 } },
+ });
+
+ IReadOnlyDictionary<string, object> received =
fixture.LastTypedConnectOptions!;
+ Assert.Equal("text", received["s"]);
+ Assert.Equal(5L, received["n"]);
+ Assert.Equal(0.5, received["d"]);
+ Assert.Equal(new byte[] { 1, 2, 3 }, received["cert"]);
+ }
+
+ [Fact]
+ public void TypedConnectOptionsFallBackToConnectForExistingProducers()
+ {
+ var fixture = new Driver11();
+ using AdbcDriver imported = Load(fixture);
+ using AdbcDatabase db = imported.Open(new Dictionary<string,
string>());
+ using AdbcConnection conn = db.ConnectWithTypedOptions(new
Dictionary<string, object>
+ {
+ { "n", 5L },
+ { "cert", new byte[] { 1, 2, 3 } },
+ });
+
+ Assert.Null(fixture.LastTypedConnectOptions);
+ Assert.Equal("5", fixture.LastConnection!.Options["n"]);
+ Assert.Equal(new byte[] { 1, 2, 3 },
fixture.LastConnection!.Options["cert"]);
+ }
+
+ [Fact]
+ public void AutoCommitSetBeforeInitIsReported()
+ {
+ var fixture = new Driver11();
+ using AdbcDriver imported = Load(fixture);
+ using AdbcDatabase db = imported.Open(new Dictionary<string,
string>());
+ using AdbcConnection conn = db.Connect(new Dictionary<string,
string>
+ {
+ { AdbcOptions.Connection.Autocommit, AdbcOptions.Disabled },
+ });
+
+ Assert.False(fixture.LastConnection!.AutoCommitAtConnect);
+ Assert.Equal(AdbcOptions.Disabled,
conn.GetOption(AdbcOptions.Connection.Autocommit));
+ Assert.False(conn.AutoCommit);
+ }
+
+ [Fact]
+ public void ExecuteUpdateRoundTripsWithoutAResultStream()
+ {
+ var fixture = new Driver11();
+ using AdbcDriver imported = Load(fixture);
+ using AdbcDatabase db = imported.Open(new Dictionary<string,
string>());
+ using AdbcConnection conn = db.Connect(null);
+ using AdbcStatement stmt = conn.CreateStatement();
+
+ stmt.SqlQuery = "UPDATE";
+ Assert.Equal(42, stmt.ExecuteUpdate().AffectedRows);
+ }
+
+ [Fact]
+ public void StatisticsAreMarshaled()
+ {
+ var fixture = new Driver11();
+ using AdbcDriver imported = Load(fixture);
+ using AdbcDatabase db = imported.Open(new Dictionary<string,
string>());
+ using AdbcConnection conn = db.Connect(null);
+
+ using (IArrowArrayStream names = conn.GetStatisticsNames())
+ {
+ Assert.Equal("statistic_name",
names.Schema.FieldsList[0].Name);
+ }
+
+ using (IArrowArrayStream stats = conn.GetStatistics("cat", null,
"tbl", approximate: true))
+ {
+ Assert.Equal("statistic_name",
stats.Schema.FieldsList[0].Name);
+ }
+ Assert.Equal(("cat", (string?)null, "tbl", true),
fixture.LastConnection!.LastStatisticsRequest);
+ }
+
+ [Fact]
+ public void ErrorDetailsArePropagated()
+ {
+ var fixture = new Driver11();
+ using AdbcDriver imported = Load(fixture);
+ using AdbcDatabase db = imported.Open(new Dictionary<string,
string>());
+ using AdbcConnection conn = db.Connect(null);
+ using AdbcStatement stmt = conn.CreateStatement();
+ stmt.SqlQuery = "FAIL";
+
+ AdbcException ex = Assert.ThrowsAny<AdbcException>(() =>
stmt.ExecuteQuery());
+ Assert.Equal(AdbcStatusCode.InvalidData, ex.Status);
+ Assert.Equal("42000", ex.SqlState);
+ Assert.Contains("query failed", ex.Message);
+ Assert.Equal(2, ex.Details.Count);
+ Assert.Equal("query_id", ex.Details[0].Key);
+ Assert.Equal("q-123",
Encoding.UTF8.GetString(ex.Details[0].Value));
+ Assert.Equal("empty", ex.Details[1].Key);
+ Assert.Empty(ex.Details[1].Value);
+ }
+
+ [Fact]
+ public void ExecutePartitionedRoundTrips()
+ {
+ var fixture = new Driver11();
+ using AdbcDriver imported = Load(fixture);
+ using AdbcDatabase db = imported.Open(new Dictionary<string,
string>());
+ using AdbcConnection conn = db.Connect(null);
+ using AdbcStatement stmt = conn.CreateStatement();
+
+ stmt.SqlQuery = "PARTITIONS";
+ PartitionedResult result = stmt.ExecutePartitioned();
+
+ Assert.Equal(5, result.AffectedRows);
+ Assert.Equal("x", Assert.Single(result.Schema.FieldsList).Name);
+ Assert.Equal(2, result.PartitionDescriptors.Count);
+ Assert.Equal(new byte[] { 1, 2 },
result.PartitionDescriptors[0].Descriptor.ToArray());
+ Assert.Equal(new byte[] { 3 },
result.PartitionDescriptors[1].Descriptor.ToArray());
+ }
+
+ [Fact]
+ public void ExecutePartitionedFailureDoesNotReleaseGarbage()
+ {
+ var fixture = new Driver11();
+ using AdbcDriver imported = Load(fixture);
+ using AdbcDatabase db = imported.Open(new Dictionary<string,
string>());
+ using AdbcConnection conn = db.Connect(null);
+ using AdbcStatement stmt = conn.CreateStatement();
+
+ // The producer fails without writing the partitions struct
+ stmt.SqlQuery = "OTHER";
+ for (int i = 0; i < 20; i++)
+ {
+ Assert.Equal(AdbcStatusCode.NotImplemented,
Assert.ThrowsAny<AdbcException>(() => stmt.ExecutePartitioned()).Status);
+ }
+ }
+
+ [Fact]
+ public void ErrorWithoutDetailsReportsVendorCode()
+ {
+ var fixture = new Driver11();
+ using AdbcDriver imported = Load(fixture);
+ using AdbcDatabase db = imported.Open(new Dictionary<string,
string>());
+ using AdbcConnection conn = db.Connect(null);
+ using AdbcStatement stmt = conn.CreateStatement();
+ stmt.SqlQuery = "FAIL_PLAIN";
+
+ AdbcException ex = Assert.ThrowsAny<AdbcException>(() =>
stmt.ExecuteQuery());
+ Assert.Equal("HY000", ex.SqlState);
+ Assert.Equal(1234, ex.NativeError);
+ Assert.Empty(ex.Details);
+ }
+
+ [Fact]
+ public void UnknownOptionIsNotFound()
+ {
+ var fixture = new Driver11();
+ using AdbcDriver imported = Load(fixture);
+ using AdbcDatabase db = imported.Open(new Dictionary<string,
string>());
+ using AdbcConnection conn = db.Connect(null);
+ using AdbcStatement stmt = conn.CreateStatement();
+
+ Assert.Equal(AdbcStatusCode.NotFound,
Assert.ThrowsAny<AdbcException>(() => conn.GetOption("nope")).Status);
+ Assert.Equal(AdbcStatusCode.NotFound,
Assert.ThrowsAny<AdbcException>(() => stmt.GetOptionInt64("nope")).Status);
+ }
+
+ [Fact]
+ public void OperationCanceledExceptionIsReportedAsCancelled()
+ {
+ using AdbcDriver imported = Load(new Driver11());
+ using AdbcDatabase db = imported.Open(new Dictionary<string,
string>());
+ using AdbcConnection conn = db.Connect(null);
+ using AdbcStatement stmt = conn.CreateStatement();
+ stmt.SqlQuery = "FAIL_CANCELED";
+
+ AdbcException ex = Assert.ThrowsAny<AdbcException>(() =>
stmt.ExecuteQuery());
+ Assert.Equal(AdbcStatusCode.Cancelled, ex.Status);
+ }
+
+ [Fact]
+ public async Task CancelReachesImportedStatement()
+ {
+ var fixture = new Driver11();
+ using AdbcDriver imported = Load(fixture);
+ using AdbcDatabase db = imported.Open(new Dictionary<string,
string>());
+ using AdbcConnection conn = db.Connect(null);
+ using AdbcStatement stmt = conn.CreateStatement();
+ stmt.SqlQuery = "BLOCK";
+
+ Task<QueryResult> pending = Task.Run(() => stmt.ExecuteQuery());
+
Assert.True(fixture.LastStatement!.Started.Wait(TimeSpan.FromSeconds(10)));
+ stmt.Cancel();
+
+ AdbcException ex = await Assert.ThrowsAnyAsync<AdbcException>(()
=> pending);
+ Assert.Equal(AdbcStatusCode.Cancelled, ex.Status);
+ }
+
+ [Fact]
+ public void Version1_0DriverFallsBackForTypedSetters()
+ {
+ var fixture = new Driver11();
+ AdbcDriverInit adapter = (int version, ref CAdbcDriver
nativeDriver, ref CAdbcError error) =>
+ {
+ if (version != AdbcVersion.Version_1_0_0) { return
AdbcStatusCode.NotImplemented; }
+ return CallExporter(version, ref nativeDriver, ref error,
fixture);
+ };
+
+ using AdbcDriver imported = CAdbcDriverImporter.Load(adapter);
+ Assert.Equal(AdbcVersion.Version_1_0_0, imported.DriverVersion);
+
+ using AdbcDatabase db = imported.Open(new Dictionary<string,
object> { { "n", 7L } });
+ Assert.Equal("7", fixture.LastDatabase!.Options["n"]);
+
+ using AdbcConnection conn = db.Connect(null);
+ conn.SetOptionInt64("timeout", 5);
+ Assert.Equal("5", fixture.LastConnection!.Options["timeout"]);
+
+ Assert.Equal(AdbcStatusCode.NotImplemented,
Assert.ThrowsAny<AdbcException>(() => conn.GetOption("timeout")).Status);
Review Comment:
This expectation locks in behavior opposite to the stated 1.0 fallback
(“getters report `NotFound`”) and the new base getter defaults. The imported
database, connection, and statement getters all access `Driver11`
unconditionally, so a negotiated 1.0 driver throws `NotImplemented` before
reaching any base fallback. Add version guards that delegate each getter to its
base implementation, then expect `NotFound` here.
--
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]