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