From 84e3eccbfbad54c0a786c8a812a8bc69d969c46c Mon Sep 17 00:00:00 2001 From: David Coe Date: Fri, 12 Jan 2024 09:58:18 -0500 Subject: [PATCH] add SHOW tables; custom keyword support; intro KeywordDefinition --- csharp/src/Client/AdbcCommand.cs | 5 +- csharp/src/Client/QueryParser.cs | 146 +++++++++--------- .../Client/QueryParserTests.cs | 42 +++-- .../Apache.Arrow.Adbc.Tests/ClientTests.cs | 37 ++--- .../Drivers/Interop/Snowflake/ClientTests.cs | 22 +++ 5 files changed, 143 insertions(+), 109 deletions(-) diff --git a/csharp/src/Client/AdbcCommand.cs b/csharp/src/Client/AdbcCommand.cs index 97efa6729d..4e7d908d35 100644 --- a/csharp/src/Client/AdbcCommand.cs +++ b/csharp/src/Client/AdbcCommand.cs @@ -31,7 +31,7 @@ public sealed class AdbcCommand : DbCommand { private AdbcStatement _adbcStatement; private int _timeout = 30; - public QueryConfiguration _queryConfiguration; + public QueryConfiguration _queryConfiguration = new QueryConfiguration(); /// /// Overloaded. Initializes . @@ -54,7 +54,6 @@ public AdbcCommand(AdbcStatement adbcStatement, AdbcConnection adbcConnection) : this._adbcStatement = adbcStatement; this.DbConnection = adbcConnection; this.DecimalBehavior = adbcConnection.DecimalBehavior; - this._queryConfiguration = new QueryConfiguration(); } /// @@ -196,7 +195,7 @@ protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) foreach(Query q in queries) { - if (q.Type == QueryType.Read) + if (q.Type == QueryReturnType.RecordSet) { if(queryResult == null) { diff --git a/csharp/src/Client/QueryParser.cs b/csharp/src/Client/QueryParser.cs index 8a8bc6ebff..d8747a8f49 100644 --- a/csharp/src/Client/QueryParser.cs +++ b/csharp/src/Client/QueryParser.cs @@ -18,7 +18,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Text.RegularExpressions; namespace Apache.Arrow.Adbc.Client @@ -28,93 +27,101 @@ namespace Apache.Arrow.Adbc.Client /// 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[] { }; + CreateKeyword = new KeywordDefinition("CREATE", QueryReturnType.RecordsAffected); + SelectKeyword = new KeywordDefinition("SELECT", QueryReturnType.RecordSet); + UpdateKeyword = new KeywordDefinition("UPDATE", QueryReturnType.RecordsAffected); + DeleteKeyword = new KeywordDefinition("DELETE", QueryReturnType.RecordsAffected); + DropKeyword = new KeywordDefinition("DROP", QueryReturnType.RecordsAffected); + InsertKeyword = new KeywordDefinition("INSERT", QueryReturnType.RecordsAffected); + + Keywords = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { CreateKeyword.Keyword, CreateKeyword.ReturnType }, + { SelectKeyword.Keyword, SelectKeyword.ReturnType }, + { UpdateKeyword.Keyword, UpdateKeyword.ReturnType }, + { DeleteKeyword.Keyword, DeleteKeyword.ReturnType }, + { DropKeyword.Keyword, DropKeyword.ReturnType }, + { InsertKeyword.Keyword, InsertKeyword.ReturnType }, + }; } /// /// The CREATE keyword. CREATE by default. /// - public string CreateKeyword { get; set; } + public KeywordDefinition CreateKeyword { get; set; } /// /// The SELECT keyword. SELECT by default. /// - public string SelectKeyword { get; set; } + public KeywordDefinition SelectKeyword { get; set; } /// /// The UPDATE keyword. UPDATE by default. /// - public string UpdateKeyword { get; set; } + public KeywordDefinition UpdateKeyword { get; set; } /// /// The INSERT keyword. INSERT by default. /// - public string InsertKeyword { get; set; } + public KeywordDefinition InsertKeyword { get; set; } /// /// The DELETE keyword. DELETE by default. /// - public string DeleteKeyword { get; set; } + public KeywordDefinition DeleteKeyword { get; set; } /// /// The DROP keyword. DROP by default. /// - public string DropKeyword { get; set; } + public KeywordDefinition DropKeyword { get; set; } /// - /// Optional additional keywords. + /// Keywords to parse from the query. Contains CREATE, SELECT, UPDATE, INSERT, DELETE, DROP by default. /// - public string[] AdditionalKeywords { get; set; } + public Dictionary Keywords { get; set; } /// - /// All of the keywords that have been passed. + /// Optional. The caller can specify their own parsing function + /// to parse the queries instead of using the default one. /// - public string[] AllKeywords - { - get - { - if(_keywords.Length > 0) - { - return _keywords; - } + public QueryFunctionDefinition CustomParser { get; set; } + } - List keywords = new List() - { - CreateKeyword, - SelectKeyword, - UpdateKeyword, - InsertKeyword, - DeleteKeyword, - DropKeyword - }; - - foreach(string kw in AdditionalKeywords) - { - keywords.Add(kw); - } + /// + /// A keyword definition. + /// + public class KeywordDefinition + { + /// + /// Overloaded. Initializes a . + /// + public KeywordDefinition() + { - _keywords = keywords.ToArray(); + } - return _keywords; - } + /// + /// Overloaded. Initializes a . + /// + /// The keyword. + /// The expected return type. + public KeywordDefinition(string keyword, QueryReturnType queryReturnType) + { + this.Keyword = keyword; + this.ReturnType = queryReturnType; } /// - /// Optional. The caller can specify their own parsing function - /// to parse the queries instead of using the default one. + /// The keyword. /// - public QueryFunctionDefinition CustomParser { get; set; } + public string Keyword { get; set; } + + /// + /// The expected return type. + /// + public QueryReturnType ReturnType { get; set; } } /// @@ -162,7 +169,7 @@ internal List ParseQuery(string commandText) } else { - userQueries = SplitStringWithKeywords(commandText, _queryConfiguration.AllKeywords); + userQueries = SplitStringWithKeywords(commandText); } return ParseQueries(userQueries); @@ -180,29 +187,26 @@ private List ParseQueries(string[] userQueries) 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"); + string firstWord = query.Text.Split(' ')[0]; - queries.Add(query); + if(this._queryConfiguration.Keywords.TryGetValue(firstWord, out QueryReturnType returnType)) + { + query.Type = returnType; + queries.Add(query); + } + else + { + throw new InvalidOperationException($"{firstWord} is not defined as a keyword"); + } } return queries; } - private string[] SplitStringWithKeywords(string input, string[] keywords) + private string[] SplitStringWithKeywords(string input) { + string[] keywords = _queryConfiguration.Keywords.Keys.ToArray(); + // Construct the regex pattern with capturing groups for keywords string pattern = $"({string.Join("|", keywords.Select(Regex.Escape))})"; @@ -230,14 +234,10 @@ private string[] SplitStringWithKeywords(string input, string[] keywords) /// /// Specifies the type of query this is /// - public enum QueryType + public enum QueryReturnType { - Create, - Read, - Insert, - Update, - Delete, - Drop + RecordSet, + RecordsAffected } /// @@ -253,6 +253,6 @@ internal class Query /// /// The query type /// - public QueryType Type { get; set; } + public QueryReturnType 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 index c666686ec5..fdd58e1f4f 100644 --- a/csharp/test/Apache.Arrow.Adbc.Tests/Client/QueryParserTests.cs +++ b/csharp/test/Apache.Arrow.Adbc.Tests/Client/QueryParserTests.cs @@ -38,50 +38,55 @@ public class QueryParserTests "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 + QueryReturnType.RecordsAffected,QueryReturnType.RecordsAffected, QueryReturnType.RecordsAffected, QueryReturnType.RecordsAffected, QueryReturnType.RecordSet, QueryReturnType.RecordSet,QueryReturnType.RecordSet, QueryReturnType.RecordsAffected )] [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 + QueryReturnType.RecordsAffected, QueryReturnType.RecordsAffected, QueryReturnType.RecordsAffected, QueryReturnType.RecordsAffected, QueryReturnType.RecordSet, QueryReturnType.RecordSet, QueryReturnType.RecordSet, QueryReturnType.RecordsAffected )] [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 + QueryReturnType.RecordsAffected, QueryReturnType.RecordsAffected, QueryReturnType.RecordsAffected, QueryReturnType.RecordsAffected, QueryReturnType.RecordSet, QueryReturnType.RecordSet, QueryReturnType.RecordSet, QueryReturnType.RecordsAffected )] [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 + QueryReturnType.RecordsAffected, QueryReturnType.RecordsAffected, QueryReturnType.RecordsAffected, QueryReturnType.RecordSet )] [InlineData( "select * from testtable where intcol=123", "select * from testtable where intcol=123", 1, - QueryType.Read + QueryReturnType.RecordSet )] [InlineData( "DELETE testtable where intcol=123", "", 1, - QueryType.Delete + QueryReturnType.RecordsAffected )] - public void ParseQuery(string query, string firstSelectQuery, int expectedQueries, params QueryType[] queryTypes) + public void ParseQuery(string query, string firstSelectQuery, int expectedQueries, params QueryReturnType[] 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(); + List keywords = new List(); + foreach (string key in customParsingQc.Keywords.Keys) + keywords.Add(key); + + // do the same with a custom parser customParsingQc.CustomParser = new QueryFunctionDefinition() { - Parameter2 = qc.AllKeywords, + Parameter2 = keywords.ToArray(), Parse = (input, values) => { // Construct the regex pattern with capturing groups for keywords @@ -111,7 +116,22 @@ public void ParseQuery(string query, string firstSelectQuery, int expectedQuerie AssertValues(customParsingQc, query, firstSelectQuery, expectedQueries, queryTypes); } - private void AssertValues(QueryConfiguration qc, string query, string firstSelectQuery, int expectedQueries, params QueryType[] queryTypes) + [Theory] + [InlineData( + "SHOW TABLES", + "SHOW TABLES", + 1, + QueryReturnType.RecordSet + )] + public void ParseQueryWithCustomKeywords(string query, string firstSelectQuery, int expectedQueries, params QueryReturnType[] queryTypes) + { + QueryConfiguration qc = new QueryConfiguration(); + qc.Keywords.Add("SHOW", QueryReturnType.RecordSet); + + AssertValues(qc, query, firstSelectQuery, expectedQueries, queryTypes); + } + + private void AssertValues(QueryConfiguration qc, string query, string firstSelectQuery, int expectedQueries, params QueryReturnType[] queryTypes) { QueryParser parser = new QueryParser(qc); @@ -127,7 +147,7 @@ private void AssertValues(QueryConfiguration qc, string query, string firstSelec 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)) + if (q.Type == QueryReturnType.RecordSet && string.IsNullOrEmpty(firstFoundSelectQuery)) { firstFoundSelectQuery = q.Text.Trim(); } diff --git a/csharp/test/Apache.Arrow.Adbc.Tests/ClientTests.cs b/csharp/test/Apache.Arrow.Adbc.Tests/ClientTests.cs index aefe6687a0..6783d77ae8 100644 --- a/csharp/test/Apache.Arrow.Adbc.Tests/ClientTests.cs +++ b/csharp/test/Apache.Arrow.Adbc.Tests/ClientTests.cs @@ -21,6 +21,7 @@ using System.Collections.ObjectModel; using System.Data; using System.Data.Common; +using System.Diagnostics; using System.Linq; using Apache.Arrow.Adbc.Client; using Apache.Arrow.Types; @@ -94,7 +95,8 @@ public static void CanClientGetSchema(Adbc.Client.AdbcConnection adbcConnection, /// /// The to use. /// The to use - public static void CanClientExecuteQuery(Adbc.Client.AdbcConnection adbcConnection, TestConfiguration testConfiguration) + /// Additional keywords, if needed. + public static void CanClientExecuteQuery(Adbc.Client.AdbcConnection adbcConnection, TestConfiguration testConfiguration, List additionalKeywords = null, bool ignoreCounts = false) { if (adbcConnection == null) throw new ArgumentNullException(nameof(adbcConnection)); if (testConfiguration == null) throw new ArgumentNullException(nameof(testConfiguration)); @@ -104,6 +106,15 @@ public static void CanClientExecuteQuery(Adbc.Client.AdbcConnection adbcConnecti adbcConnection.Open(); using AdbcCommand adbcCommand = new AdbcCommand(testConfiguration.Query, adbcConnection); + + if(additionalKeywords != null) + { + foreach(KeywordDefinition definition in additionalKeywords) + { + adbcCommand.QueryConfiguration.Keywords.Add(definition.Keyword, definition.ReturnType); + } + } + using AdbcDataReader reader = adbcCommand.ExecuteReader(); try @@ -121,12 +132,14 @@ public static void CanClientExecuteQuery(Adbc.Client.AdbcConnection adbcConnecti // write out the values to ensure things like null are correctly returned Console.WriteLine($"{reader.GetName(i)}: {value}"); + Debug.WriteLine($"{reader.GetName(i)}: {value}"); } } } finally { reader.Close(); } - Assert.Equal(testConfiguration.ExpectedResultsCount, count); + if(!ignoreCounts) + Assert.Equal(testConfiguration.ExpectedResultsCount, count); } /// @@ -143,16 +156,6 @@ public static void CanClientExecuteDeleteQuery(Adbc.Client.AdbcConnection adbcCo 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 @@ -177,16 +180,6 @@ public static void CanClientExecuteMultipleQueries(Adbc.Client.AdbcConnection ad 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 diff --git a/csharp/test/Drivers/Interop/Snowflake/ClientTests.cs b/csharp/test/Drivers/Interop/Snowflake/ClientTests.cs index 063e4b94ec..3925fe469f 100644 --- a/csharp/test/Drivers/Interop/Snowflake/ClientTests.cs +++ b/csharp/test/Drivers/Interop/Snowflake/ClientTests.cs @@ -126,6 +126,28 @@ public void CanClientExecuteQuery() } } + /// + /// Validates if the client can connect to a live server + /// and parse the results. + /// + [SkippableFact, Order(4)] + public void CanClientExecuteShowTables() + { + SnowflakeTestConfiguration testConfiguration = Utils.LoadTestConfiguration(SnowflakeTestingUtils.SNOWFLAKE_TEST_CONFIG_VARIABLE); + + using (Adbc.Client.AdbcConnection adbcConnection = GetSnowflakeAdbcConnectionUsingConnectionString(testConfiguration)) + { + testConfiguration.Query = "SHOW TABLES"; + + List additionalKeywords = new List() + { + new KeywordDefinition("SHOW", QueryReturnType.RecordSet) + }; + + Tests.ClientTests.CanClientExecuteQuery(adbcConnection, testConfiguration, additionalKeywords, true); + } + } + // /// Validates if the client can connect to a live server /// and parse the results.