diff --git a/csharp/src/Client/AdbcCommand.cs b/csharp/src/Client/AdbcCommand.cs
index 09bd920532..97efa6729d 100644
--- a/csharp/src/Client/AdbcCommand.cs
+++ b/csharp/src/Client/AdbcCommand.cs
@@ -16,28 +16,22 @@
*/
using System;
+using System.Collections.Generic;
using System.Data;
using System.Data.Common;
+using System.Linq;
using System.Threading.Tasks;
namespace Apache.Arrow.Adbc.Client
{
- public enum AdbcCommandType
- {
- Create,
- Read,
- Update,
- Delete
- }
-
///
/// Creates an ADO.NET command over an Adbc statement.
///
public sealed class AdbcCommand : DbCommand
{
- private AdbcStatement adbcStatement;
+ private AdbcStatement _adbcStatement;
private int _timeout = 30;
- private AdbcCommandType _adbcCommandType = AdbcCommandType.Read;
+ public QueryConfiguration _queryConfiguration;
///
/// Overloaded. Initializes .
@@ -57,9 +51,10 @@ public AdbcCommand(AdbcStatement adbcStatement, AdbcConnection adbcConnection) :
if(adbcConnection == null)
throw new ArgumentNullException(nameof(adbcConnection));
- this.adbcStatement = adbcStatement;
+ this._adbcStatement = adbcStatement;
this.DbConnection = adbcConnection;
this.DecimalBehavior = adbcConnection.DecimalBehavior;
+ this._queryConfiguration = new QueryConfiguration();
}
///
@@ -70,12 +65,12 @@ public AdbcCommand(AdbcStatement adbcStatement, AdbcConnection adbcConnection) :
public AdbcCommand(string query, AdbcConnection adbcConnection) : base()
{
if (string.IsNullOrEmpty(query))
- throw new ArgumentNullException(nameof(adbcStatement));
+ throw new ArgumentNullException(nameof(_adbcStatement));
if (adbcConnection == null)
throw new ArgumentNullException(nameof(adbcConnection));
- this.adbcStatement = adbcConnection.AdbcStatement;
+ this._adbcStatement = adbcConnection.AdbcStatement;
this.CommandText = query;
this.DbConnection = adbcConnection;
@@ -86,20 +81,20 @@ public AdbcCommand(string query, AdbcConnection adbcConnection) : base()
/// Gets the associated with
/// this .
///
- public AdbcStatement AdbcStatement => this.adbcStatement;
+ public AdbcStatement AdbcStatement => this._adbcStatement;
public DecimalBehavior DecimalBehavior { get; set; }
public override string CommandText
{
- get => this.adbcStatement.SqlQuery;
- set => this.adbcStatement.SqlQuery = value;
+ get => this._adbcStatement.SqlQuery;
+ set => this._adbcStatement.SqlQuery = value;
}
- public AdbcCommandType AdbcCommandType
+ public QueryConfiguration QueryConfiguration
{
- get => this._adbcCommandType;
- set => this._adbcCommandType = value;
+ get => this._queryConfiguration;
+ set => this._queryConfiguration = value;
}
public override CommandType CommandType
@@ -108,7 +103,6 @@ public override CommandType CommandType
{
return CommandType.Text;
}
-
set
{
if (value != CommandType.Text)
@@ -129,8 +123,8 @@ public override int CommandTimeout
///
public byte[] SubstraitPlan
{
- get => this.adbcStatement.SubstraitPlan;
- set => this.adbcStatement.SubstraitPlan = value;
+ get => this._adbcStatement.SubstraitPlan;
+ set => this._adbcStatement.SubstraitPlan = value;
}
protected override DbConnection DbConnection { get; set; }
@@ -145,7 +139,7 @@ public override int ExecuteNonQuery()
///
public UpdateResult ExecuteUpdate()
{
- return this.adbcStatement.ExecuteUpdate();
+ return this._adbcStatement.ExecuteUpdate();
}
///
@@ -154,7 +148,7 @@ public UpdateResult ExecuteUpdate()
///
public QueryResult ExecuteQuery()
{
- QueryResult executed = this.adbcStatement.ExecuteQuery();
+ QueryResult executed = this._adbcStatement.ExecuteQuery();
return executed;
}
@@ -186,15 +180,49 @@ protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior)
{
case CommandBehavior.SchemaOnly: // The schema is not known until a read happens
case CommandBehavior.Default:
- if (this.AdbcCommandType == AdbcCommandType.Read)
+
+ // ADBC doesn't have very good support for multi-statements
+ // see https://github.com/apache/arrow-adbc/issues/1358
+ // so this attempts to work around that by making multiple calls
+ // it will return the first result set and the "RecordsAffected" for any other type of calls
+
+ if (this.QueryConfiguration != null)
{
- QueryResult result = this.ExecuteQuery();
- return new AdbcDataReader(this, result, this.DecimalBehavior);
+ QueryParser queryParser = new QueryParser(this.QueryConfiguration);
+ List queries = queryParser.ParseQuery(this.CommandText);
+
+ QueryResult queryResult = null;
+ int recordsEffected = -1;
+
+ foreach(Query q in queries)
+ {
+ if (q.Type == QueryType.Read)
+ {
+ if(queryResult == null)
+ {
+ this._adbcStatement.SqlQuery = q.Text;
+ queryResult = this.ExecuteQuery();
+ }
+ }
+ else
+ {
+ if(recordsEffected == -1)
+ recordsEffected++;
+
+ this._adbcStatement.SqlQuery = q.Text;
+ recordsEffected += this.ExecuteNonQuery();
+ }
+ }
+
+ if (queryResult != null)
+ return new AdbcDataReader(this, queryResult, this.DecimalBehavior, recordsEffected);
+ else
+ return new AdbcDataReader(recordsEffected);
}
else
{
- UpdateResult result = this.ExecuteUpdate();
- return new AdbcDataReader(result);
+ QueryResult result = this.ExecuteQuery();
+ return new AdbcDataReader(this, result, this.DecimalBehavior);
}
default:
@@ -207,7 +235,7 @@ protected override void Dispose(bool disposing)
if(disposing)
{
// TODO: ensure not in the middle of pulling
- this.adbcStatement?.Dispose();
+ this._adbcStatement?.Dispose();
}
base.Dispose(disposing);
diff --git a/csharp/src/Client/AdbcDataReader.cs b/csharp/src/Client/AdbcDataReader.cs
index ac7002c7da..3c314253a4 100644
--- a/csharp/src/Client/AdbcDataReader.cs
+++ b/csharp/src/Client/AdbcDataReader.cs
@@ -45,7 +45,7 @@ public sealed class AdbcDataReader : DbDataReader, IDbColumnSchemaGenerator
// this is only set if it's not a SELECT statement
private int recordsEffected = -1;
- internal AdbcDataReader(AdbcCommand adbcCommand, QueryResult adbcQueryResult, DecimalBehavior decimalBehavior)
+ internal AdbcDataReader(AdbcCommand adbcCommand, QueryResult adbcQueryResult, DecimalBehavior decimalBehavior, int recordsEffected=-1)
{
if (adbcCommand == null)
throw new ArgumentNullException(nameof(adbcCommand));
@@ -62,14 +62,12 @@ internal AdbcDataReader(AdbcCommand adbcCommand, QueryResult adbcQueryResult, De
this.isClosed = false;
this.DecimalBehavior = decimalBehavior;
+ this.recordsEffected = recordsEffected;
}
- internal AdbcDataReader(UpdateResult updateResult)
+ internal AdbcDataReader(int recordsEffected)
{
- if (updateResult == null)
- throw new ArgumentNullException(nameof(updateResult));
-
- this.recordsEffected = Convert.ToInt32(updateResult.AffectedRows);
+ this.recordsEffected = recordsEffected;
}
public override object this[int ordinal] => GetValue(ordinal);
diff --git a/csharp/src/Client/AssemblyInfo.cs b/csharp/src/Client/AssemblyInfo.cs
new file mode 100644
index 0000000000..cea36f5123
--- /dev/null
+++ b/csharp/src/Client/AssemblyInfo.cs
@@ -0,0 +1,18 @@
+// 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.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Apache.Arrow.Adbc.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e504183f6d470d6b67b6d19212be3e1f598f70c246a120194bc38130101d0c1853e4a0f2232cb12e37a7a90e707aabd38511dac4f25fcb0d691b2aa265900bf42de7f70468fc997551a40e1e0679b605aa2088a4a69e07c117e988f5b1738c570ee66997fba02485e7856a49eca5fd0706d09899b8312577cbb9034599fc92d4")]
diff --git a/csharp/src/Client/QueryParser.cs b/csharp/src/Client/QueryParser.cs
new file mode 100644
index 0000000000..8a8bc6ebff
--- /dev/null
+++ b/csharp/src/Client/QueryParser.cs
@@ -0,0 +1,258 @@
+/*
+* 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.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace Apache.Arrow.Adbc.Client
+{
+ ///
+ /// Provides a way for the caller to specify how queries are parsed.
+ ///
+ public class QueryConfiguration
+ {
+ private string[] _keywords = new string[] { };
+
+ public QueryConfiguration()
+ {
+ CreateKeyword = "CREATE";
+ SelectKeyword = "SELECT";
+ UpdateKeyword = "UPDATE";
+ DeleteKeyword = "DELETE";
+ DropKeyword = "DROP";
+ InsertKeyword = "INSERT";
+
+ this.AdditionalKeywords = new string[] { };
+ }
+
+ ///
+ /// The CREATE keyword. CREATE by default.
+ ///
+ public string CreateKeyword { get; set; }
+
+ ///
+ /// The SELECT keyword. SELECT by default.
+ ///
+ public string SelectKeyword { get; set; }
+
+ ///
+ /// The UPDATE keyword. UPDATE by default.
+ ///
+ public string UpdateKeyword { get; set; }
+
+ ///
+ /// The INSERT keyword. INSERT by default.
+ ///
+ public string InsertKeyword { get; set; }
+
+ ///
+ /// The DELETE keyword. DELETE by default.
+ ///
+ public string DeleteKeyword { get; set; }
+
+ ///
+ /// The DROP keyword. DROP by default.
+ ///
+ public string DropKeyword { get; set; }
+
+ ///
+ /// Optional additional keywords.
+ ///
+ public string[] AdditionalKeywords { get; set; }
+
+ ///
+ /// All of the keywords that have been passed.
+ ///
+ public string[] AllKeywords
+ {
+ get
+ {
+ if(_keywords.Length > 0)
+ {
+ return _keywords;
+ }
+
+ List keywords = new List()
+ {
+ CreateKeyword,
+ SelectKeyword,
+ UpdateKeyword,
+ InsertKeyword,
+ DeleteKeyword,
+ DropKeyword
+ };
+
+ foreach(string kw in AdditionalKeywords)
+ {
+ keywords.Add(kw);
+ }
+
+ _keywords = keywords.ToArray();
+
+ return _keywords;
+ }
+ }
+
+ ///
+ /// Optional. The caller can specify their own parsing function
+ /// to parse the queries instead of using the default one.
+ ///
+ public QueryFunctionDefinition CustomParser { get; set; }
+ }
+
+ ///
+ /// Defines the function definition for a custom parser.
+ ///
+ public class QueryFunctionDefinition
+ {
+ ///
+ /// The second parameter for the function.
+ ///
+ public string[] Parameter2 { get; set; }
+
+ ///
+ /// The custom function to call.
+ ///
+ ///
+ /// Input 1 is always the query.
+ ///
+ /// Input 2 is customizable using .
+ ///
+ /// The return type is a string[]
+ ///
+ public Func Parse { get; set; }
+ }
+
+ ///
+ /// Parses a command text into multiple queries.
+ ///
+ internal class QueryParser
+ {
+ private QueryConfiguration _queryConfiguration = new QueryConfiguration();
+
+ public QueryParser(QueryConfiguration queryConfiguration)
+ {
+ _queryConfiguration = queryConfiguration;
+ }
+
+ internal List ParseQuery(string commandText)
+ {
+ string[] userQueries = null;
+
+ if(_queryConfiguration.CustomParser != null)
+ {
+ userQueries = _queryConfiguration.CustomParser.Parse(commandText, _queryConfiguration.CustomParser.Parameter2);
+ }
+ else
+ {
+ userQueries = SplitStringWithKeywords(commandText, _queryConfiguration.AllKeywords);
+ }
+
+ return ParseQueries(userQueries);
+ }
+
+ private List ParseQueries(string[] userQueries)
+ {
+ List queries = new List();
+
+ foreach (string userQuery in userQueries)
+ {
+ if (string.IsNullOrEmpty(userQuery))
+ continue;
+
+ Query query = new Query();
+ query.Text = userQuery.Trim();
+
+ if (query.Text.ToUpper().StartsWith(_queryConfiguration.CreateKeyword))
+ query.Type = QueryType.Create;
+ else if (query.Text.ToUpper().StartsWith(_queryConfiguration.SelectKeyword))
+ query.Type = QueryType.Read;
+ else if (query.Text.ToUpper().StartsWith(_queryConfiguration.InsertKeyword))
+ query.Type = QueryType.Insert;
+ else if (query.Text.ToUpper().StartsWith(_queryConfiguration.UpdateKeyword))
+ query.Type = QueryType.Update;
+ else if (query.Text.ToUpper().StartsWith(_queryConfiguration.DeleteKeyword))
+ query.Type = QueryType.Delete;
+ else if (query.Text.ToUpper().StartsWith(_queryConfiguration.DropKeyword))
+ query.Type = QueryType.Drop;
+ else
+ throw new InvalidOperationException("unable to parse query");
+
+ queries.Add(query);
+ }
+
+ return queries;
+ }
+
+ private string[] SplitStringWithKeywords(string input, string[] keywords)
+ {
+ // Construct the regex pattern with capturing groups for keywords
+ string pattern = $"({string.Join("|", keywords.Select(Regex.Escape))})";
+
+ string[] result = Regex.Split(input, pattern, RegexOptions.IgnoreCase);
+
+ // add back in the keyword that was found
+ for (int i = 1; i < result.Length; i += 2)
+ {
+ if (i < result.Length - 1)
+ {
+ result[i] += result[i + 1];
+ }
+ }
+
+ // Remove empty entries
+ result = result
+ .Where(s => !string.IsNullOrWhiteSpace(s))
+ .Where((_, index) => index % 2 == 0) // Only keep entries with even indices (keyword-query pairs)
+ .ToArray();
+
+ return result;
+ }
+ }
+
+ ///
+ /// Specifies the type of query this is
+ ///
+ public enum QueryType
+ {
+ Create,
+ Read,
+ Insert,
+ Update,
+ Delete,
+ Drop
+ }
+
+ ///
+ /// Represents a query to the backend specifying the text and type
+ ///
+ internal class Query
+ {
+ ///
+ /// The query text.
+ ///
+ public string Text { get; set; }
+
+ ///
+ /// The query type
+ ///
+ public QueryType Type { get; set; }
+ }
+}
diff --git a/csharp/test/Apache.Arrow.Adbc.Tests/Client/QueryParserTests.cs b/csharp/test/Apache.Arrow.Adbc.Tests/Client/QueryParserTests.cs
new file mode 100644
index 0000000000..c666686ec5
--- /dev/null
+++ b/csharp/test/Apache.Arrow.Adbc.Tests/Client/QueryParserTests.cs
@@ -0,0 +1,142 @@
+/*
+* 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.Linq;
+using System.Text.RegularExpressions;
+using Apache.Arrow.Adbc.Client;
+using Xunit;
+
+namespace Apache.Arrow.Adbc.Tests.Client
+{
+ public class QueryParserTests
+ {
+ ///
+ /// Tests the ability to parse a query.
+ ///
+ /// The query to parse.
+ /// The expected select query, if present.
+ /// The number of expected queries from parsing the query.
+ /// The expected query types.
+ [Theory]
+ [InlineData(
+ "DROP TABLE IF EXISTS TESTTABLE;CREATE TABLE TESTTABLE(intcol INT);INSERT INTO TESTTABLE VALUES (123);INSERT INTO TESTTABLE VALUES (456);SELECT * FROM TESTTABLE WHERE INTCOL=123;SELECT * FROM TESTTABLE WHERE INTCOL=456;SELECT * FROM TESTTABLE WHERE INTCOL=456;DROP TABLE TESTTABLE;",
+ "SELECT * FROM TESTTABLE WHERE INTCOL=123;",
+ 8,
+ QueryType.Drop,QueryType.Create, QueryType.Insert, QueryType.Insert,QueryType.Read, QueryType.Read,QueryType.Read, QueryType.Drop
+ )]
+ [InlineData(
+ "DROP TABLE IF EXISTS TESTTABLE;CREATE TABLE TESTTABLE(intcol INT);INSERT INTO TESTTABLE VALUES (123);INSERT INTO TESTTABLE VALUES (456);SELECT * FROM TESTTABLE WHERE INTCOL=123;SELECT * FROM TESTTABLE WHERE INTCOL='item21;item2;item3';SELECT * FROM TESTTABLE WHERE INTCOL=456;DROP TABLE TESTTABLE;",
+ "SELECT * FROM TESTTABLE WHERE INTCOL=123;",
+ 8,
+ QueryType.Drop, QueryType.Create, QueryType.Insert, QueryType.Insert, QueryType.Read, QueryType.Read, QueryType.Read, QueryType.Drop
+ )]
+ [InlineData(
+ "drop table if exists testtable;create table testtable(intcol int);insert into testtable values (123);insert into testtable values (456);select * from testtable where intcol=123;select * from testtable where intcol='item21;item2;item3';select * from testtable where intcol=456;drop table testtable;",
+ "select * from testtable where intcol=123;",
+ 8,
+ QueryType.Drop, QueryType.Create, QueryType.Insert, QueryType.Insert, QueryType.Read, QueryType.Read, QueryType.Read, QueryType.Drop
+ )]
+ [InlineData(
+ "CREATE OR REPLACE TRANSIENT TABLE TESTTABLE(intcol INT);INSERT INTO TESTTABLE VALUES (123);INSERT INTO TESTTABLE VALUES (456);SELECT * FROM TESTTABLE WHERE INTCOL=123;",
+ "SELECT * FROM TESTTABLE WHERE INTCOL=123;",
+ 4,
+ QueryType.Create, QueryType.Insert, QueryType.Insert, QueryType.Read
+ )]
+ [InlineData(
+ "select * from testtable where intcol=123",
+ "select * from testtable where intcol=123",
+ 1,
+ QueryType.Read
+ )]
+ [InlineData(
+ "DELETE testtable where intcol=123",
+ "",
+ 1,
+ QueryType.Delete
+ )]
+ public void ParseQuery(string query, string firstSelectQuery, int expectedQueries, params QueryType[] queryTypes)
+ {
+ // uses the defaults
+ QueryConfiguration qc = new QueryConfiguration();
+
+ AssertValues(qc, query, firstSelectQuery, expectedQueries, queryTypes);
+
+ // do the same with a custom parser
+ QueryConfiguration customParsingQc = new QueryConfiguration();
+ customParsingQc.CustomParser = new QueryFunctionDefinition()
+ {
+ Parameter2 = qc.AllKeywords,
+ Parse = (input, values) =>
+ {
+ // Construct the regex pattern with capturing groups for keywords
+ string pattern = $"({string.Join("|", values.Select(Regex.Escape))})";
+
+ string[] result = Regex.Split(input, pattern, RegexOptions.IgnoreCase);
+
+ // add back in the keyword that was found
+ for (int i = 1; i < result.Length; i += 2)
+ {
+ if (i < result.Length - 1)
+ {
+ result[i] += result[i + 1];
+ }
+ }
+
+ // Remove empty entries
+ result = result
+ .Where(s => !string.IsNullOrWhiteSpace(s))
+ .Where((_, index) => index % 2 == 0) // Only keep entries with even indices (keyword-query pairs)
+ .ToArray();
+
+ return result;
+ }
+ };
+
+ AssertValues(customParsingQc, query, firstSelectQuery, expectedQueries, queryTypes);
+ }
+
+ private void AssertValues(QueryConfiguration qc, string query, string firstSelectQuery, int expectedQueries, params QueryType[] queryTypes)
+ {
+ QueryParser parser = new QueryParser(qc);
+
+ List queries = parser.ParseQuery(query);
+
+ string firstFoundSelectQuery = string.Empty;
+
+ Assert.True(queries.Count == expectedQueries, $"The number of queries ({queries.Count}) does not match the expected number ({expectedQueries})");
+
+ for (int i = 0; i < queries.Count; i++)
+ {
+ Query q = queries[i];
+
+ Assert.True(q.Type == queryTypes[i], $"The value at {i} ({q.Type}) does not match the expected query type ({queryTypes[i]})");
+
+ if (q.Type == QueryType.Read && string.IsNullOrEmpty(firstFoundSelectQuery))
+ {
+ firstFoundSelectQuery = q.Text.Trim();
+ }
+ }
+
+ if (!string.IsNullOrEmpty(firstSelectQuery))
+ {
+ Assert.True(firstSelectQuery.Equals(firstFoundSelectQuery, StringComparison.OrdinalIgnoreCase), "The expected queries do not match");
+ }
+ }
+ }
+}
diff --git a/csharp/test/Apache.Arrow.Adbc.Tests/ClientTests.cs b/csharp/test/Apache.Arrow.Adbc.Tests/ClientTests.cs
index 2b45df0eb7..aefe6687a0 100644
--- a/csharp/test/Apache.Arrow.Adbc.Tests/ClientTests.cs
+++ b/csharp/test/Apache.Arrow.Adbc.Tests/ClientTests.cs
@@ -143,7 +143,15 @@ public static void CanClientExecuteDeleteQuery(Adbc.Client.AdbcConnection adbcCo
adbcConnection.Open();
using AdbcCommand adbcCommand = new AdbcCommand(testConfiguration.Query, adbcConnection);
- adbcCommand.AdbcCommandType = AdbcCommandType.Delete;
+ adbcCommand.QueryConfiguration = new QueryConfiguration()
+ {
+ CreateKeyword = "CREATE",
+ SelectKeyword = "SELECT",
+ UpdateKeyword = "UPDATE",
+ DeleteKeyword = "DELETE",
+ DropKeyword = "DROP",
+ InsertKeyword = "INSERT"
+ };
using AdbcDataReader reader = adbcCommand.ExecuteReader();
@@ -155,6 +163,51 @@ public static void CanClientExecuteDeleteQuery(Adbc.Client.AdbcConnection adbcCo
finally { reader.Close(); }
}
+ ///
+ /// Validates if the client can connect to a live server and
+ /// parse the results.
+ ///
+ /// The to use.
+ /// The to use
+ public static void CanClientExecuteMultipleQueries(Adbc.Client.AdbcConnection adbcConnection, TestConfiguration testConfiguration)
+ {
+ if (adbcConnection == null) throw new ArgumentNullException(nameof(adbcConnection));
+ if (testConfiguration == null) throw new ArgumentNullException(nameof(testConfiguration));
+
+ adbcConnection.Open();
+
+ using AdbcCommand adbcCommand = new AdbcCommand(testConfiguration.Query, adbcConnection);
+ adbcCommand.QueryConfiguration = new QueryConfiguration()
+ {
+ CreateKeyword = "CREATE",
+ SelectKeyword = "SELECT",
+ UpdateKeyword = "UPDATE",
+ DeleteKeyword = "DELETE",
+ DropKeyword = "DROP",
+ InsertKeyword = "INSERT"
+ };
+
+ using AdbcDataReader reader = adbcCommand.ExecuteReader();
+
+ try
+ {
+ int count = 0;
+
+ while(reader.Read())
+ {
+ count += 1;
+ }
+
+ // the expectation is the number of RecordsAffected = the number inserted
+ Assert.Equal(count, testConfiguration.ExpectedResultsCount);
+
+ // the expectation is you insert X records + delete X records so it's doubled
+ Assert.Equal(reader.RecordsAffected, testConfiguration.ExpectedResultsCount * 2);
+ }
+ finally { reader.Close(); }
+ }
+
+
///
/// Validates if the client is retrieving and converting values
/// to the expected types.
diff --git a/csharp/test/Drivers/Interop/Snowflake/ClientTests.cs b/csharp/test/Drivers/Interop/Snowflake/ClientTests.cs
index 0c4b6debee..063e4b94ec 100644
--- a/csharp/test/Drivers/Interop/Snowflake/ClientTests.cs
+++ b/csharp/test/Drivers/Interop/Snowflake/ClientTests.cs
@@ -234,6 +234,33 @@ public void CanClientDeleteRecords()
}
}
+ [SkippableFact, Order(9)]
+ public void CanClientExecuteMultipleQueries()
+ {
+ SnowflakeTestConfiguration testConfiguration = Utils.LoadTestConfiguration(SnowflakeTestingUtils.SNOWFLAKE_TEST_CONFIG_VARIABLE);
+
+ using (Adbc.Client.AdbcConnection adbcConnection = GetSnowflakeAdbcConnectionUsingConnectionString(testConfiguration))
+ {
+ string multi_query = string.Empty;
+
+ string[] queries = SnowflakeTestingUtils.GetQueries(testConfiguration);
+
+ foreach(string query in queries)
+ {
+ multi_query += query;
+
+ if(!multi_query.EndsWith(";"))
+ multi_query += ";";
+ }
+
+ multi_query += $"SELECT * FROM {testConfiguration.Metadata.Catalog}.{testConfiguration.Metadata.Schema}.{testConfiguration.Metadata.Table};";
+ multi_query += $"DELETE FROM {testConfiguration.Metadata.Catalog}.{testConfiguration.Metadata.Schema}.{testConfiguration.Metadata.Table}";
+ testConfiguration.Query = multi_query;
+
+ Tests.ClientTests.CanClientExecuteMultipleQueries(adbcConnection, testConfiguration);
+ }
+ }
+
private Adbc.Client.AdbcConnection GetSnowflakeAdbcConnectionUsingConnectionString(SnowflakeTestConfiguration testConfiguration, string authType = null)
{
// see https://arrow.apache.org/adbc/0.5.1/driver/snowflake.html