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

curth 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 927922fda feat(csharp/src/Client): parse custom properties from 
connection string (#2352)
927922fda is described below

commit 927922fdaeb2f2344a6f7e41508cf5e791b47b24
Author: davidhcoe <[email protected]>
AuthorDate: Fri Dec 6 11:40:31 2024 -0500

    feat(csharp/src/Client): parse custom properties from connection string 
(#2352)
    
    Follow up to https://github.com/apache/arrow-adbc/pull/2312 to add
    parsing of the timeout values and other custom properties like
    StructBehavior and DecimalBehavior from the connection string.
    
    ---------
    
    Co-authored-by: David Coe <[email protected]>
    Co-authored-by: Bruce Irschick <[email protected]>
---
 csharp/src/Client/AdbcCommand.cs                   |   9 +-
 csharp/src/Client/AdbcConnection.cs                |  46 ++++++++-
 csharp/src/Client/ConnectionStringParser.cs        |  81 +++++++++++++++
 csharp/src/Client/readme.md                        |   9 ++
 .../Apache.Arrow.Adbc.Tests/Client/ClientTests.cs  | 113 +++++++++++++++++++++
 5 files changed, 255 insertions(+), 3 deletions(-)

diff --git a/csharp/src/Client/AdbcCommand.cs b/csharp/src/Client/AdbcCommand.cs
index c3695feaf..a317ca19c 100644
--- a/csharp/src/Client/AdbcCommand.cs
+++ b/csharp/src/Client/AdbcCommand.cs
@@ -53,6 +53,7 @@ namespace Apache.Arrow.Adbc.Client
 
             this.DbConnection = adbcConnection;
             this.DecimalBehavior = adbcConnection.DecimalBehavior;
+            this.StructBehavior = adbcConnection.StructBehavior;
             this._adbcStatement = adbcConnection.CreateStatement();
         }
 
@@ -74,6 +75,7 @@ namespace Apache.Arrow.Adbc.Client
 
             this.DbConnection = adbcConnection;
             this.DecimalBehavior = adbcConnection.DecimalBehavior;
+            this.StructBehavior = adbcConnection.StructBehavior;
         }
 
         // For testing
@@ -83,6 +85,12 @@ namespace Apache.Arrow.Adbc.Client
             this.DbConnection = adbcConnection;
             this.DecimalBehavior = adbcConnection.DecimalBehavior;
             this.StructBehavior = adbcConnection.StructBehavior;
+
+            if (adbcConnection.CommandTimeoutValue != null)
+            {
+                this.AdbcCommandTimeoutProperty = 
adbcConnection.CommandTimeoutValue.DriverPropertyName;
+                this.CommandTimeout = adbcConnection.CommandTimeoutValue.Value;
+            }
         }
 
         /// <summary>
@@ -119,7 +127,6 @@ namespace Apache.Arrow.Adbc.Client
             }
         }
 
-
         /// <summary>
         /// Gets or sets the name of the command timeout property for the 
underlying ADBC driver.
         /// </summary>
diff --git a/csharp/src/Client/AdbcConnection.cs 
b/csharp/src/Client/AdbcConnection.cs
index 2b35c92d9..a8d805f7e 100644
--- a/csharp/src/Client/AdbcConnection.cs
+++ b/csharp/src/Client/AdbcConnection.cs
@@ -34,6 +34,7 @@ namespace Apache.Arrow.Adbc.Client
     {
         private AdbcDatabase? adbcDatabase;
         private Adbc.AdbcConnection? adbcConnectionInternal;
+        private TimeoutValue? connectionTimeoutValue;
 
         private readonly Dictionary<string, string> adbcConnectionParameters;
         private readonly Dictionary<string, string> adbcConnectionOptions;
@@ -122,6 +123,8 @@ namespace Apache.Arrow.Adbc.Client
             return this.adbcConnectionInternal!.CreateStatement();
         }
 
+        internal TimeoutValue? CommandTimeoutValue { get; private set; }
+
 #if NET5_0_OR_GREATER
         [AllowNull]
 #endif
@@ -137,11 +140,30 @@ namespace Apache.Arrow.Adbc.Client
         /// </summary>
         public StructBehavior StructBehavior { get; set; } = 
StructBehavior.JsonString;
 
+        public override int ConnectionTimeout
+        {
+            get
+            {
+                if (connectionTimeoutValue != null)
+                    return connectionTimeoutValue.Value;
+                else
+                    return base.ConnectionTimeout;
+            }
+        }
+
         protected override DbCommand CreateDbCommand()
         {
             EnsureConnectionOpen();
 
-            return new AdbcCommand(this);
+            AdbcCommand cmd = new AdbcCommand(this);
+
+            if (CommandTimeoutValue != null)
+            {
+                cmd.AdbcCommandTimeoutProperty = 
CommandTimeoutValue.DriverPropertyName!;
+                cmd.CommandTimeout = CommandTimeoutValue.Value;
+            }
+
+            return cmd;
         }
 
         /// <summary>
@@ -237,7 +259,27 @@ namespace Apache.Arrow.Adbc.Client
                 object? builderValue = builder[key];
                 if (builderValue != null)
                 {
-                    this.adbcConnectionParameters.Add(key, 
Convert.ToString(builderValue)!);
+                    string paramValue = Convert.ToString(builderValue)!;
+
+                    switch (key)
+                    {
+                        case ConnectionStringKeywords.DecimalBehavior:
+                            this.DecimalBehavior = 
(DecimalBehavior)Enum.Parse(typeof(DecimalBehavior), paramValue);
+                            break;
+                        case ConnectionStringKeywords.StructBehavior:
+                            this.StructBehavior = 
(StructBehavior)Enum.Parse(typeof(StructBehavior), paramValue);
+                            break;
+                        case ConnectionStringKeywords.CommandTimeout:
+                            CommandTimeoutValue = 
ConnectionStringParser.ParseTimeoutValue(paramValue);
+                            break;
+                        case ConnectionStringKeywords.ConnectionTimeout:
+                            this.connectionTimeoutValue = 
ConnectionStringParser.ParseTimeoutValue(paramValue);
+                            
this.adbcConnectionParameters[connectionTimeoutValue.DriverPropertyName] = 
connectionTimeoutValue.Value.ToString();
+                            break;
+                        default:
+                            this.adbcConnectionParameters.Add(key, paramValue);
+                            break;
+                    }
                 }
             }
         }
diff --git a/csharp/src/Client/ConnectionStringParser.cs 
b/csharp/src/Client/ConnectionStringParser.cs
new file mode 100644
index 000000000..3fb602870
--- /dev/null
+++ b/csharp/src/Client/ConnectionStringParser.cs
@@ -0,0 +1,81 @@
+/*
+* 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.Text.RegularExpressions;
+
+namespace Apache.Arrow.Adbc.Client
+{
+    internal class ConnectionStringKeywords
+    {
+        public const string ConnectionTimeout = "adbcconnectiontimeout";
+        public const string CommandTimeout = "adbccommandtimeout";
+        public const string StructBehavior = "structbehavior";
+        public const string DecimalBehavior = "decimalbehavior";
+    }
+
+    internal class ConnectionStringParser
+    {
+        public static TimeoutValue ParseTimeoutValue(string value)
+        {
+            string pattern = @"\(([^,]+),\s*([^,]+),\s*([^,]+)\)";
+
+            // Match the regex
+            Match match = Regex.Match(value, pattern);
+
+            if (match.Success)
+            {
+                string driverPropertyName = match.Groups[1].Value.Trim();
+                string timeoutAsString = match.Groups[2].Value.Trim();
+                string units = match.Groups[3].Value.Trim();
+
+                if (units != "s" && units != "ms")
+                {
+                    throw new InvalidOperationException("invalid units");
+                }
+
+                TimeoutValue timeoutValue = new TimeoutValue
+                {
+                    DriverPropertyName = driverPropertyName,
+                    Value = int.Parse(timeoutAsString),
+                    Units = units
+                };
+
+                return timeoutValue;
+            }
+            else
+            {
+                throw new ArgumentOutOfRangeException(nameof(value));
+            }
+        }
+    }
+
+    internal class TimeoutValue
+    {
+        public string DriverPropertyName { get; set; } = string.Empty;
+
+        public int Value { get; set; }
+
+        // seconds=s
+        // milliseconds=ms
+        /// <remarks>
+        /// While these can be helpful, the DbConnection and DbCommand
+        /// objects limit the use of these.
+        /// </remarks>
+        public string Units { get; set; } = string.Empty;
+    }
+}
diff --git a/csharp/src/Client/readme.md b/csharp/src/Client/readme.md
index 2c4a51aca..59f647a37 100644
--- a/csharp/src/Client/readme.md
+++ b/csharp/src/Client/readme.md
@@ -73,3 +73,12 @@ if using the default user name and password authentication, 
but look like
 when using JWT authentication with an unencrypted key file.
 
 Other ADBC drivers will have different connection parameters, so be sure to 
check the documentation for each driver.
+
+### Connection Keywords
+Because the ADO.NET client is designed to work with multiple drivers, callers 
will need to specify the driver properties that are set for particular values. 
This can be done either as properties on the objects directly, or can be parsed 
from the connection string.
+These properties are:
+
+- __AdbcConnectionTimeout__ - This specifies the connection timeout value. The 
value needs to be in the form (driver.property.name, integer, unit) where the 
unit is one of `s` or `ms`, For example, 
`AdbcConnectionTimeout=(adbc.snowflake.sql.client_option.client_timeout,30,s)` 
would set the connection timeout to 30 seconds.
+- __AdbcCommandTimeout__ - This specifies the command timeout value. This 
follows the same pattern as `AdbcConnectionTimeout` and sets the 
`AdbcCommandTimeoutProperty` and `CommandTimeout` values on the `AdbcCommand` 
object.
+- __StructBehavior__ - This specifies the StructBehavior when working with 
Arrow Struct arrays. The valid values are `JsonString` (the default) or 
`Strict` (treat the struct as a native type).
+- __DecimalBehavior__ - This specifies the DecimalBehavior when parsing 
decimal values from Arrow libraries. The valid values are `UseSqlDecimal` or 
`OverflowDecimalAsString` where values like Decimal256 are treated as strings.
diff --git a/csharp/test/Apache.Arrow.Adbc.Tests/Client/ClientTests.cs 
b/csharp/test/Apache.Arrow.Adbc.Tests/Client/ClientTests.cs
index e20b270d0..e1d5a9178 100644
--- a/csharp/test/Apache.Arrow.Adbc.Tests/Client/ClientTests.cs
+++ b/csharp/test/Apache.Arrow.Adbc.Tests/Client/ClientTests.cs
@@ -17,6 +17,7 @@
 
 using System;
 using System.Collections.Generic;
+using System.ComponentModel;
 using System.Data.SqlTypes;
 using System.Linq;
 using System.Threading;
@@ -26,6 +27,7 @@ using Apache.Arrow.Ipc;
 using Apache.Arrow.Types;
 using Moq;
 using Xunit;
+using AdbcClient = Apache.Arrow.Adbc.Client;
 
 namespace Apache.Arrow.Adbc.Tests.Client
 {
@@ -166,6 +168,117 @@ namespace Apache.Arrow.Adbc.Tests.Client
             AdbcDataReader reader = cmd.ExecuteReader();
             return reader;
         }
+
+        [Theory]
+        [InlineData("(adbc.driver.value, 1, s)", "adbc.driver.value", 1, "s", 
true)]
+        [InlineData("(somevalue,10, ms)", "somevalue", 10, "ms", true)]
+        [InlineData("(somevalue,10, s)", "somevalue", 10, "s", true)]
+        [InlineData("somevalue,10, s)", null, null, null, false)]
+        [InlineData("(somevalue,10, s", null, null, null, false)]
+        [InlineData("(some.value_goes.here,99,Q)", null, null, null, false)]
+        [InlineData("some.value_goes.here,99,Q", null, null, null, false)]
+        public void TestTimeoutParsing(string value, string? 
driverPropertyName, int? timeout, string? unit, bool success)
+        {
+            if (!success)
+            {
+                try
+                {
+                    ConnectionStringParser.ParseTimeoutValue(value);
+                }
+                catch (ArgumentOutOfRangeException) { }
+                catch (InvalidOperationException) { }
+                catch
+                {
+                    Assert.Fail("Unknown exception found");
+                }
+            }
+            else
+            {
+                Assert.True(driverPropertyName != null);
+                Assert.True(timeout != null);
+                Assert.True(unit != null);
+
+                TimeoutValue timeoutValue = 
ConnectionStringParser.ParseTimeoutValue(value);
+
+                Assert.Equal(driverPropertyName, 
timeoutValue.DriverPropertyName);
+                Assert.Equal(timeout, timeoutValue.Value);
+                Assert.Equal(unit, timeoutValue.Units);
+            }
+        }
+
+        [Theory]
+        [ClassData(typeof(ConnectionParsingTestData))]
+        internal void TestConnectionStringParsing(ConnectionStringExample 
connectionStringExample)
+        {
+            AdbcClient.AdbcConnection cn = new 
AdbcClient.AdbcConnection(connectionStringExample.ConnectionString);
+
+            Mock<AdbcStatement> mockStatement = new Mock<AdbcStatement>();
+            AdbcCommand cmd = new AdbcCommand(mockStatement.Object, cn);
+
+            Assert.True(cn.StructBehavior == 
connectionStringExample.ExpectedStructBehavior);
+            Assert.True(cn.DecimalBehavior == 
connectionStringExample.ExpectedDecimalBehavior);
+            Assert.True(cn.ConnectionTimeout == 
connectionStringExample.ConnectionTimeout);
+
+            if 
(!string.IsNullOrEmpty(connectionStringExample.CommandTimeoutProperty))
+            {
+                Assert.True(cmd.AdbcCommandTimeoutProperty == 
connectionStringExample.CommandTimeoutProperty);
+                Assert.True(cmd.CommandTimeout == 
connectionStringExample.CommandTimeout);
+            }
+            else
+            {
+                Assert.Throws<InvalidOperationException>(() => 
cmd.AdbcCommandTimeoutProperty);
+            }
+        }
+    }
+
+    internal class ConnectionStringExample
+    {
+        public ConnectionStringExample(
+            string connectionString,
+            DecimalBehavior decimalBehavior,
+            StructBehavior structBehavior,
+            string connectionTimeoutPropertyName,
+            int connectionTimeout,
+            string commandTimeoutPropertyName,
+            int commandTimeout)
+        {
+            ConnectionString = connectionString;
+            ExpectedDecimalBehavior = decimalBehavior;
+            ExpectedStructBehavior = structBehavior;
+            ConnectionTimeoutProperty = connectionTimeoutPropertyName;
+            ConnectionTimeout = connectionTimeout;
+            CommandTimeoutProperty = commandTimeoutPropertyName;
+            CommandTimeout = commandTimeout;
+        }
+
+        public string ConnectionString { get; }
+
+        public string ConnectionTimeoutProperty { get; }
+
+        public int ConnectionTimeout { get; }
+
+        public DecimalBehavior ExpectedDecimalBehavior { get; }
+
+        public StructBehavior ExpectedStructBehavior { get; }
+
+        public string CommandTimeoutProperty { get; }
+
+        public int CommandTimeout { get; }
+    }
+
+    /// <summary>
+    /// Collection of <see cref="ConnectionStringExample"/> for testing 
statement timeouts."/>
+    /// </summary>
+    internal class ConnectionParsingTestData : 
TheoryData<ConnectionStringExample>
+    {
+        public ConnectionParsingTestData()
+        {
+            int defaultDbConnectionTimeout = 15;
+
+            Add(new("StructBehavior=JsonString", default, 
StructBehavior.JsonString, "", defaultDbConnectionTimeout, "", 30));
+            
Add(new("StructBehavior=JsonString;AdbcCommandTimeout=(adbc.apache.statement.query_timeout_s,45,s)",
 default, StructBehavior.JsonString, "", defaultDbConnectionTimeout, 
"adbc.apache.statement.query_timeout_s", 45));
+            
Add(new("StructBehavior=JsonString;DecimalBehavior=OverflowDecimalAsString;AdbcConnectionTimeout=(adbc.spark.connect_timeout_ms,90,s);AdbcCommandTimeout=(adbc.apache.statement.query_timeout_s,45,s)",
 DecimalBehavior.OverflowDecimalAsString, StructBehavior.JsonString, 
"adbc.spark.connect_timeout_ms", 90, "adbc.apache.statement.query_timeout_s", 
45));
+        }
     }
 
     class MockArrayStream : IArrowArrayStream

Reply via email to