From dff91f37100467c8f9b5629324b2e8daf3c2cabf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20L=2E=20Charlier?= Date: Sat, 27 Jan 2024 21:02:53 +0100 Subject: [PATCH] feat: connection to SingleStore (#812) * feat: connection to SingleStore * test: do not run SingleStore QA on AppVeyor * docs: update automatically generated documentation related to schemes --------- Co-authored-by: AppVeyor bot --- .../Mapping/Database/SingleStoreDatabase.cs | 18 ++++ .../Implementation/SingleStoreMapper.cs | 25 +++++ .../Dialects/Renderers/MySqlRenderer.cs | 3 + .../Dialects/Renderers/SingleStoreRenderer.cs | 19 ++++ .../Querying/Dialects/SingleStoreDialect.cs | 20 ++++ .../Implementation/SingleStoreRewriter.cs | 71 ++++++++++++++ DubUrl.QA/DubUrl.QA.csproj | 9 ++ DubUrl.QA/SelectAllCustomer.singlestore.sql | 1 + DubUrl.QA/SelectCustomerById.singlestore.sql | 1 + DubUrl.QA/SelectFirstCustomer.singlestore.sql | 1 + .../SelectYoungestCustomers.singlestore.sql | 8 ++ .../SingleStore/AdoProviderSingleStore.cs | 38 ++++++++ DubUrl.QA/SingleStore/OdbcDriverMariaDb.cs | 25 +++++ DubUrl.QA/SingleStore/OdbcDriverMySQL.cs | 25 +++++ .../deploy-singlestore-database.sql | 18 ++++ .../deploy-singlestore-test-env.ps1 | 55 +++++++++++ .../SingleStore/run-singlestore-docker.cmd | 1 + DubUrl.Testing/DubUrl.Testing.csproj | 1 + .../Mapping/SchemeMapperBuilderTest.cs | 5 + .../Implementation/SingleStoreRewriterTest.cs | 94 +++++++++++++++++++ README.md | 8 +- appveyor.yml | 4 +- docs/_data/natives.json | 12 +++ 23 files changed, 460 insertions(+), 2 deletions(-) create mode 100644 DubUrl.Core/Mapping/Database/SingleStoreDatabase.cs create mode 100644 DubUrl.Core/Mapping/Implementation/SingleStoreMapper.cs create mode 100644 DubUrl.Core/Querying/Dialects/Renderers/SingleStoreRenderer.cs create mode 100644 DubUrl.Core/Querying/Dialects/SingleStoreDialect.cs create mode 100644 DubUrl.Core/Rewriting/Implementation/SingleStoreRewriter.cs create mode 100644 DubUrl.QA/SelectAllCustomer.singlestore.sql create mode 100644 DubUrl.QA/SelectCustomerById.singlestore.sql create mode 100644 DubUrl.QA/SelectFirstCustomer.singlestore.sql create mode 100644 DubUrl.QA/SelectYoungestCustomers.singlestore.sql create mode 100644 DubUrl.QA/SingleStore/AdoProviderSingleStore.cs create mode 100644 DubUrl.QA/SingleStore/OdbcDriverMariaDb.cs create mode 100644 DubUrl.QA/SingleStore/OdbcDriverMySQL.cs create mode 100644 DubUrl.QA/SingleStore/deploy-singlestore-database.sql create mode 100644 DubUrl.QA/SingleStore/deploy-singlestore-test-env.ps1 create mode 100644 DubUrl.QA/SingleStore/run-singlestore-docker.cmd create mode 100644 DubUrl.Testing/Rewriting/Implementation/SingleStoreRewriterTest.cs diff --git a/DubUrl.Core/Mapping/Database/SingleStoreDatabase.cs b/DubUrl.Core/Mapping/Database/SingleStoreDatabase.cs new file mode 100644 index 00000000..a387c128 --- /dev/null +++ b/DubUrl.Core/Mapping/Database/SingleStoreDatabase.cs @@ -0,0 +1,18 @@ +using DubUrl.Querying.Dialects; +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DubUrl.Mapping.Database; + +[Database( + "SingleStore" + , ["singlestore", "single"] + , DatabaseCategory.Warehouse +)] +[Brand("singlestore", "#AA00FF")] +public class SingleStoreDatabase : IDatabase +{ } diff --git a/DubUrl.Core/Mapping/Implementation/SingleStoreMapper.cs b/DubUrl.Core/Mapping/Implementation/SingleStoreMapper.cs new file mode 100644 index 00000000..83bce895 --- /dev/null +++ b/DubUrl.Core/Mapping/Implementation/SingleStoreMapper.cs @@ -0,0 +1,25 @@ +using DubUrl.Mapping.Database; +using DubUrl.Querying.Dialects; +using DubUrl.Querying.Parametrizing; +using DubUrl.Rewriting.Implementation; +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DubUrl.Mapping.Implementation; + +[Mapper( + "SingleStoreConnector" +)] +public class SingleStoreMapper : BaseMapper +{ + public SingleStoreMapper(DbConnectionStringBuilder csb, IDialect dialect, IParametrizer parametrizer) + : base(new SingleStoreRewriter(csb), + dialect, + parametrizer + ) + { } +} diff --git a/DubUrl.Core/Querying/Dialects/Renderers/MySqlRenderer.cs b/DubUrl.Core/Querying/Dialects/Renderers/MySqlRenderer.cs index 63791ec0..31925efe 100644 --- a/DubUrl.Core/Querying/Dialects/Renderers/MySqlRenderer.cs +++ b/DubUrl.Core/Querying/Dialects/Renderers/MySqlRenderer.cs @@ -14,4 +14,7 @@ public MySqlRenderer() .With(new FunctionFormatter("TIME", new IntervalAsTimeFormatter())) , new NullFormatter() , new IdentifierBacktickFormatter()) { } + + protected MySqlRenderer(BaseValueFormatter value) + : base(value, new NullFormatter(), new IdentifierBacktickFormatter()) { } } diff --git a/DubUrl.Core/Querying/Dialects/Renderers/SingleStoreRenderer.cs b/DubUrl.Core/Querying/Dialects/Renderers/SingleStoreRenderer.cs new file mode 100644 index 00000000..8f501e90 --- /dev/null +++ b/DubUrl.Core/Querying/Dialects/Renderers/SingleStoreRenderer.cs @@ -0,0 +1,19 @@ +using DubUrl.Querying.Dialects.Formatters; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DubUrl.Querying.Dialects.Renderers; + +internal class SingleStoreRenderer : MySqlRenderer +{ + public SingleStoreRenderer() + : base(new ValueFormatter() + .With(new FunctionFormatter("TIME", new IntervalAsTimeFormatter())) + .With(new FunctionFormatter("DATE", new DateFormatter())) + .With(new FunctionFormatter("TIME", new TimeFormatter())) + .With(new FunctionFormatter("TIMESTAMP", new TimestampFormatter())) + ) { } +} diff --git a/DubUrl.Core/Querying/Dialects/SingleStoreDialect.cs b/DubUrl.Core/Querying/Dialects/SingleStoreDialect.cs new file mode 100644 index 00000000..def16b63 --- /dev/null +++ b/DubUrl.Core/Querying/Dialects/SingleStoreDialect.cs @@ -0,0 +1,20 @@ +using DubUrl.Querying.Dialects.Casters; +using DubUrl.Querying.Dialects.Renderers; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DubUrl.Querying.Dialects; + +[Renderer()] +[ReturnCaster] +[ReturnCaster>] +[ReturnCaster>] +[ParentLanguage] +public class SingleStoreDialect : BaseDialect +{ + internal SingleStoreDialect(ILanguage language, string[] aliases, IRenderer renderer, ICaster[] casters) + : base(language, aliases, renderer, casters) { } +} diff --git a/DubUrl.Core/Rewriting/Implementation/SingleStoreRewriter.cs b/DubUrl.Core/Rewriting/Implementation/SingleStoreRewriter.cs new file mode 100644 index 00000000..e002ea10 --- /dev/null +++ b/DubUrl.Core/Rewriting/Implementation/SingleStoreRewriter.cs @@ -0,0 +1,71 @@ +using DubUrl.Parsing; +using DubUrl.Rewriting.Tokening; +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DubUrl.Rewriting.Implementation; + +internal class SingleStoreRewriter : ConnectionStringRewriter +{ + private const string EXCEPTION_DATABASE_NAME = "SingleStore"; + protected internal const string SERVER_KEYWORD = "Server"; + protected internal const string PORT_KEYWORD = "Port"; + protected internal const string DATABASE_KEYWORD = "Database"; + protected internal const string USERNAME_KEYWORD = "User ID"; + protected internal const string PASSWORD_KEYWORD = "Password"; + + public SingleStoreRewriter(DbConnectionStringBuilder csb) + : base(new UniqueAssignmentSpecificator(csb), + [ + new HostMapper(), + new AuthentificationMapper(), + new DatabaseMapper(), + new OptionsMapper(), + ] + ) + { } + + protected SingleStoreRewriter(Specificator specificator, BaseTokenMapper[] tokenMappers) + : base(specificator, tokenMappers) { } + + internal class HostMapper : BaseTokenMapper + { + public override void Execute(UrlInfo urlInfo) + { + Specificator.Execute(SERVER_KEYWORD, urlInfo.Host); + if (urlInfo.Port > 0) + Specificator.Execute(PORT_KEYWORD, urlInfo.Port); + } + } + + internal class AuthentificationMapper : BaseTokenMapper + { + public override void Execute(UrlInfo urlInfo) + { + if (!string.IsNullOrEmpty(urlInfo.Username)) + Specificator.Execute(USERNAME_KEYWORD, urlInfo.Username); + if (!string.IsNullOrEmpty(urlInfo.Password)) + Specificator.Execute(PASSWORD_KEYWORD, urlInfo.Password); + + if (string.IsNullOrEmpty(urlInfo.Username)) + throw new UsernameNotFoundException(); + } + } + + internal class DatabaseMapper : BaseTokenMapper + { + public override void Execute(UrlInfo urlInfo) + { + if (urlInfo.Segments==null || !urlInfo.Segments.Any()) + throw new InvalidConnectionUrlMissingSegmentsException(EXCEPTION_DATABASE_NAME); + else if (urlInfo.Segments.Length == 1) + Specificator.Execute(DATABASE_KEYWORD, urlInfo.Segments.First()); + else if (urlInfo.Segments.Length > 1) + throw new InvalidConnectionUrlTooManySegmentsException(EXCEPTION_DATABASE_NAME, urlInfo.Segments); + } + } +} diff --git a/DubUrl.QA/DubUrl.QA.csproj b/DubUrl.QA/DubUrl.QA.csproj index dbb1b029..207d86a6 100644 --- a/DubUrl.QA/DubUrl.QA.csproj +++ b/DubUrl.QA/DubUrl.QA.csproj @@ -6,12 +6,17 @@ + + + + + @@ -23,6 +28,8 @@ + + @@ -53,6 +60,7 @@ + @@ -77,6 +85,7 @@ + diff --git a/DubUrl.QA/SelectAllCustomer.singlestore.sql b/DubUrl.QA/SelectAllCustomer.singlestore.sql new file mode 100644 index 00000000..ac47766e --- /dev/null +++ b/DubUrl.QA/SelectAllCustomer.singlestore.sql @@ -0,0 +1 @@ +select * from Customer \ No newline at end of file diff --git a/DubUrl.QA/SelectCustomerById.singlestore.sql b/DubUrl.QA/SelectCustomerById.singlestore.sql new file mode 100644 index 00000000..921cb5a3 --- /dev/null +++ b/DubUrl.QA/SelectCustomerById.singlestore.sql @@ -0,0 +1 @@ +select FullName from Customer where CustomerId=? \ No newline at end of file diff --git a/DubUrl.QA/SelectFirstCustomer.singlestore.sql b/DubUrl.QA/SelectFirstCustomer.singlestore.sql new file mode 100644 index 00000000..632cb752 --- /dev/null +++ b/DubUrl.QA/SelectFirstCustomer.singlestore.sql @@ -0,0 +1 @@ +select FullName from Customer where CustomerId=1 \ No newline at end of file diff --git a/DubUrl.QA/SelectYoungestCustomers.singlestore.sql b/DubUrl.QA/SelectYoungestCustomers.singlestore.sql new file mode 100644 index 00000000..9caa6c08 --- /dev/null +++ b/DubUrl.QA/SelectYoungestCustomers.singlestore.sql @@ -0,0 +1,8 @@ +select + * +from + Customer +order by + BirthDate desc +limit + ? \ No newline at end of file diff --git a/DubUrl.QA/SingleStore/AdoProviderSingleStore.cs b/DubUrl.QA/SingleStore/AdoProviderSingleStore.cs new file mode 100644 index 00000000..950ec41b --- /dev/null +++ b/DubUrl.QA/SingleStore/AdoProviderSingleStore.cs @@ -0,0 +1,38 @@ +using NUnit.Framework; +using System.Diagnostics; +using System.Data; +using System.Data.Common; +using DubUrl.Querying; +using DubUrl.Querying.Reading; +using DubUrl.Registering; +using System.Reflection; + +namespace DubUrl.QA.SingleStore; + +[Category("SingleStore")] +[Category("AdoProvider")] +public class AdoProviderSingleStore : BaseAdoProvider +{ + public override string ConnectionString + => $"singlestore://root:Password12!@localhost/DubUrl"; + + [Test] + public override void QueryCustomer() + => QueryCustomer("select FullName from Customer where CustomerId=1"); + + [Test] + public override void QueryCustomerWithDatabase() + => QueryCustomerWithDatabase("select FullName from Customer where CustomerId=1"); + + [Test] + public override void QueryCustomerWithParams() + => QueryCustomerWithParams("select FullName from Customer where CustomerId=@CustId"); + + [Test] + public override void QueryCustomerWithPositionalParameter() + => QueryCustomerWithPositionalParameter("select FullName from Customer where CustomerId=?"); + + [Test] + public override void QueryCustomerWithDapper() + => QueryCustomerWithDapper("select * from Customer"); +} diff --git a/DubUrl.QA/SingleStore/OdbcDriverMariaDb.cs b/DubUrl.QA/SingleStore/OdbcDriverMariaDb.cs new file mode 100644 index 00000000..580b047c --- /dev/null +++ b/DubUrl.QA/SingleStore/OdbcDriverMariaDb.cs @@ -0,0 +1,25 @@ +using NUnit.Framework; +using System.Diagnostics; +using System.Data; +using System.Data.Common; +using DubUrl.Registering; + +namespace DubUrl.QA.SingleStore; + +[Category("SingleStore")] +[Category("MariaDBDriver")] +public class OdbcDriverMariaDb : BaseOdbcDriver +{ + public override string ConnectionString + { + get => $"odbc+singlestore://root:Password12!@localhost/DubUrl"; + } + + [Test] + public override void QueryCustomer() + => QueryCustomer("select FullName from Customer where CustomerId=1"); + + [Test] + public override void QueryCustomerWithParams() + => QueryCustomerWithParams("select FullName from Customer where CustomerId=?"); +} diff --git a/DubUrl.QA/SingleStore/OdbcDriverMySQL.cs b/DubUrl.QA/SingleStore/OdbcDriverMySQL.cs new file mode 100644 index 00000000..08e882d0 --- /dev/null +++ b/DubUrl.QA/SingleStore/OdbcDriverMySQL.cs @@ -0,0 +1,25 @@ +using NUnit.Framework; +using System.Diagnostics; +using System.Data; +using System.Data.Common; +using DubUrl.Registering; + +namespace DubUrl.QA.SingleStore; + +[Category("SingleStore")] +[Category("MySQLDriver")] +public class OdbcDriverMySQL : BaseOdbcDriver +{ + public override string ConnectionString + { + get => $"odbc+singlestore://root:Password12!@localhost/DubUrl"; + } + + [Test] + public override void QueryCustomer() + => QueryCustomer("select FullName from Customer where CustomerId=1"); + + [Test] + public override void QueryCustomerWithParams() + => QueryCustomerWithParams("select FullName from Customer where CustomerId=?"); +} diff --git a/DubUrl.QA/SingleStore/deploy-singlestore-database.sql b/DubUrl.QA/SingleStore/deploy-singlestore-database.sql new file mode 100644 index 00000000..a308070b --- /dev/null +++ b/DubUrl.QA/SingleStore/deploy-singlestore-database.sql @@ -0,0 +1,18 @@ +DROP DATABASE IF EXISTS DubUrl; +CREATE DATABASE DubUrl; + +USE DubUrl; + +CREATE TABLE Customer ( + CustomerId INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + FullName VARCHAR(50), + BirthDate DATE +); + +INSERT INTO Customer (FullName, BirthDate) VALUES + ('Nikola Tesla', '1856-07-10') + ,('Albert Einstein', '1879-03-14') + ,('John von Neumann', '1903-12-28') + ,('Alan Turing', '1912-06-23') + ,('Linus Torvalds', '1969-12-28') +; \ No newline at end of file diff --git a/DubUrl.QA/SingleStore/deploy-singlestore-test-env.ps1 b/DubUrl.QA/SingleStore/deploy-singlestore-test-env.ps1 new file mode 100644 index 00000000..2ee9c2cd --- /dev/null +++ b/DubUrl.QA/SingleStore/deploy-singlestore-test-env.ps1 @@ -0,0 +1,55 @@ +Param( + [switch] $force=$false + , [string[]] $odbcDrivers = @("MariaDB", "MySQL") + , [string] $config = "Release" + , [string[]] $frameworks = @("net6.0", "net7.0") +) +. $PSScriptRoot\..\Run-TestSuite.ps1 +. $PSScriptRoot\..\Docker-Container.ps1 +. $PSScriptRoot\..\Windows-Service.ps1 + +if ($force) { + Write-Host "Enforcing QA testing for SingleStore" +} + +$filesChanged = & git diff --name-only HEAD HEAD~1 +if ($force -or ($filesChanged -like "*singlestore*")) { + Write-Host "Deploying SingleStore testing environment" + + # Starting docker container + $previouslyRunning, $running = Deploy-Container -FullName "singlestore" + if (!$previouslyRunning){ + $waitForAvailable = 3 + Write-host "`tWaiting $waitForAvailable seconds for the SingleStore server to be available ..." + Start-Sleep -s $waitForAvailable + Write-host "`tSingleStore Server is expected to be available." + } + + # Deploying database based on script + Write-host "`tCreating database using remote client on the docker container" + & docker exec -it singlestoredb-dev singlestore -pPassword12! "--execute=$(Get-Content .\deploy-singlestore-database.sql)" | Out-Null + Write-host "`tDatabase created" + + $odbcDriversInstalled = @() + # Installing ODBC driver + # Should call a single script to install MariaDB and MySQL ODBC drivers + + # Running QA tests + Write-Host "Running QA tests related to SingleStore" + $suites = @("SingleStore+AdoProvider") + foreach ($odbcDriverInstalled in $odbcDriversInstalled) { + $suites += "SingleStore+ODBC+" + $odbcDriverInstalled + "Driver" + } + $testSuccessful = Run-TestSuite $suites -config $config -frameworks $frameworks + + # Stopping DB Service + if (!$previouslyRunning) + { + Remove-Container $running + } + + # Raise failing tests + exit $testSuccessful +} else { + return -1 +} diff --git a/DubUrl.QA/SingleStore/run-singlestore-docker.cmd b/DubUrl.QA/SingleStore/run-singlestore-docker.cmd new file mode 100644 index 00000000..56f7bfd6 --- /dev/null +++ b/DubUrl.QA/SingleStore/run-singlestore-docker.cmd @@ -0,0 +1 @@ +docker run -d --name singlestoredb-dev -e SINGLESTORE_LICENSE="%SINGLESTORE_LICENSE%" -e ROOT_PASSWORD="Password12!" -p 3306:3306 -p 8080:8080 -p 9000:9000 ghcr.io/singlestore-labs/singlestoredb-dev:latest diff --git a/DubUrl.Testing/DubUrl.Testing.csproj b/DubUrl.Testing/DubUrl.Testing.csproj index eea429b3..8ac79b89 100644 --- a/DubUrl.Testing/DubUrl.Testing.csproj +++ b/DubUrl.Testing/DubUrl.Testing.csproj @@ -14,6 +14,7 @@ + diff --git a/DubUrl.Testing/Mapping/SchemeMapperBuilderTest.cs b/DubUrl.Testing/Mapping/SchemeMapperBuilderTest.cs index 67862521..7363eb60 100644 --- a/DubUrl.Testing/Mapping/SchemeMapperBuilderTest.cs +++ b/DubUrl.Testing/Mapping/SchemeMapperBuilderTest.cs @@ -14,6 +14,7 @@ using System.Text; using System.Threading.Tasks; using DubUrl.Rewriting.Implementation; +using SingleStoreConnector; namespace DubUrl.Testing.Mapping; @@ -33,6 +34,8 @@ public void DefaultRegistration() DbProviderFactories.RegisterFactory("FirebirdSql.Data.FirebirdClient", FirebirdSql.Data.FirebirdClient.FirebirdClientFactory.Instance); DbProviderFactories.RegisterFactory("System.Data.Odbc", System.Data.Odbc.OdbcFactory.Instance); DbProviderFactories.RegisterFactory("NReco.PrestoAdo", NReco.PrestoAdo.PrestoDbFactory.Instance); + DbProviderFactories.RegisterFactory("SingleStoreConnector", SingleStoreConnectorFactory.Instance); + DbProviderFactories.RegisterFactory("DuckDB.NET.Data", DuckDB.NET.Data.DuckDBClientFactory.Instance); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) DbProviderFactories.RegisterFactory("System.Data.OleDb", System.Data.OleDb.OleDbFactory.Instance); } @@ -76,6 +79,8 @@ public FakeRewriter(DbConnectionStringBuilder csb) [TestCase("ts", typeof(TimescaleMapper))] [TestCase("quest", typeof(QuestDbMapper))] [TestCase("tr", typeof(TrinoMapper))] + [TestCase("single", typeof(SingleStoreMapper))] + [TestCase("duck", typeof(DuckdbMapper))] [TestCase("odbc+mssql", typeof(OdbcMapper))] [TestCase("mssql+odbc", typeof(OdbcMapper))] public void Instantiate_Scheme_CorrectType(string scheme, Type expected) diff --git a/DubUrl.Testing/Rewriting/Implementation/SingleStoreRewriterTest.cs b/DubUrl.Testing/Rewriting/Implementation/SingleStoreRewriterTest.cs new file mode 100644 index 00000000..f4e0a57f --- /dev/null +++ b/DubUrl.Testing/Rewriting/Implementation/SingleStoreRewriterTest.cs @@ -0,0 +1,94 @@ +using DubUrl.Parsing; +using NUnit.Framework; +using System.Data.Common; +using MySqlConnector; +using DubUrl.Rewriting.Implementation; +using DubUrl.Rewriting; +using SingleStoreConnector; + +namespace DubUrl.Testing.Rewriting.Implementation; + +public class SingleStoreRewriterTest +{ + private const string PROVIDER_NAME = "SingleStoreConnector"; + + private static DbConnectionStringBuilder ConnectionStringBuilder + => ConnectionStringBuilderHelper.Retrieve(PROVIDER_NAME, SingleStoreConnectorFactory.Instance); + + [Test] + [TestCase("host", "host")] + [TestCase("host", "host", "db", 1234)] + public void Map_UrlInfo_DataSource(string expected, string host = "host", string segmentsList = "db", int port = 0) + { + var urlInfo = new UrlInfo() { Host = host, Port = port, Segments = segmentsList.Split('/'), Username = "user" }; + var Rewriter = new SingleStoreRewriter(ConnectionStringBuilder); + var result = Rewriter.Execute(urlInfo); + + Assert.That(result, Is.Not.Null); + Assert.That(result, Does.ContainKey(SingleStoreRewriter.SERVER_KEYWORD)); + Assert.That(result[SingleStoreRewriter.SERVER_KEYWORD], Is.EqualTo(expected)); + } + + [Test] + [TestCase("db")] + public void Map_UrlInfo_Database(string segmentsList = "db", string expected = "db") + { + var urlInfo = new UrlInfo() { Segments = segmentsList.Split('/'), Username = "user" }; + var Rewriter = new SingleStoreRewriter(ConnectionStringBuilder); + var result = Rewriter.Execute(urlInfo); + + Assert.That(result, Is.Not.Null); + Assert.That(result, Does.ContainKey(SingleStoreRewriter.DATABASE_KEYWORD)); + Assert.That(result[SingleStoreRewriter.DATABASE_KEYWORD], Is.EqualTo(expected)); + } + + + [Test] + public void Map_UrlInfoWithUsernamePassword_Authentication() + { + var urlInfo = new UrlInfo() { Username = "user", Password = "pwd", Segments = ["db"] }; + var Rewriter = new SingleStoreRewriter(ConnectionStringBuilder); + var result = Rewriter.Execute(urlInfo); + + Assert.That(result, Is.Not.Null); + Assert.That(result, Does.ContainKey(SingleStoreRewriter.USERNAME_KEYWORD)); + Assert.Multiple(() => + { + Assert.That(result[SingleStoreRewriter.USERNAME_KEYWORD], Is.EqualTo("user")); + Assert.That(result, Does.ContainKey(SingleStoreRewriter.PASSWORD_KEYWORD)); + }); + Assert.Multiple(() => + { + Assert.That(result[SingleStoreRewriter.PASSWORD_KEYWORD], Is.EqualTo("pwd")); + Assert.That(result, Does.Not.ContainKey("Integrated Security")); + }); + } + + [Test] + public void Map_UrlInfoWithoutUsernamePassword_Authentication() + { + var urlInfo = new UrlInfo() { Username = "", Password = "", Segments = ["db"] }; + var Rewriter = new SingleStoreRewriter(ConnectionStringBuilder); + Assert.Catch(() => Rewriter.Execute(urlInfo)); + } + + [Test] + public void Map_UrlInfo_Options() + { + var urlInfo = new UrlInfo() { Username = "user", Segments = ["db"] }; + urlInfo.Options.Add("Application Name", "myApp"); + urlInfo.Options.Add("Persist Security Info", "true"); + + var Rewriter = new SingleStoreRewriter(ConnectionStringBuilder); + var result = Rewriter.Execute(urlInfo); + + Assert.That(result, Is.Not.Null); + Assert.That(result, Does.ContainKey("Application Name")); + Assert.Multiple(() => + { + Assert.That(result["Application Name"], Is.EqualTo("myApp")); + Assert.That(result, Does.ContainKey("Persist Security Info")); + }); + Assert.That(result["Persist Security Info"], Is.True); + } +} diff --git a/README.md b/README.md index 4be52b7e..aeef46d9 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ DubUrl provides a standard, URL style mechanism for parsing database connection [![Top language](https://img.shields.io/github/languages/top/seddryck/DubUrl.svg)](https://github.com/Seddryck/DubUrl/search?l=C%23) -[![Mappers for ADO.Net Provider implemented badge](https://img.shields.io/badge/Mappers%20for%20ADO.Net%20Provider-15%20implemented-green)](https://seddryck.github.io/DubUrl/docs/native-ado-net-providers) +[![Mappers for ADO.Net Provider implemented badge](https://img.shields.io/badge/Mappers%20for%20ADO.Net%20Provider-16%20implemented-green)](https://seddryck.github.io/DubUrl/docs/native-ado-net-providers) [![Mappers for ODBC drivers implemented badge](https://img.shields.io/badge/Mappers%20for%20ODBC%20drivers-11%20implemented-green)](https://seddryck.github.io/DubUrl/docs/odbc-driver-locators) [![Mappers for OLE DB providers implemented badge](https://img.shields.io/badge/Mappers%20for%20OLE%20DB%20providers-6%20implemented-green)](https://seddryck.github.io/DubUrl/docs/oledb-provider-locators) [![Mappers for ADOMD.NET providers implemented badge](https://img.shields.io/badge/Mappers%20for%20ADOMD.NET%20providers-5%20implemented-green)](https://seddryck.github.io/DubUrl/docs/adomd-providers) @@ -129,6 +129,7 @@ The following databases and their associated schemes are supported out of the bo |![Firebird SQL](https://img.shields.io/badge/Firebird%20SQL-333333?logo=&logoColor=ffffff&style=flat-square) | fb, firebird | FirebirdSql.Data.FirebirdClient| |![SQLite3](https://img.shields.io/badge/SQLite3-003B57?logo=sqlite&logoColor=ffffff&style=flat-square) | sq, sqlite | Microsoft.Data.Sqlite | |![CockRoachDB](https://img.shields.io/badge/CockRoachDB-6933FF?logo=cockroachlabs&logoColor=ffffff&style=flat-square) | cr, cockroach, cockroachdb, crdb, cdb | Npgsql | +|![SingleStore](https://img.shields.io/badge/SingleStore-AA00FF?logo=singlestore&logoColor=ffffff&style=flat-square) | singlestore, single | SingleStoreConnector | |![Snowflake](https://img.shields.io/badge/Snowflake-29B5E8?logo=snowflake&logoColor=ffffff&style=flat-square) | sf, snowflake | Snowflake.Data | |![Teradata](https://img.shields.io/badge/Teradata-F37440?logo=Teradata&logoColor=ffffff&style=flat-square) | td, teradata, tera | Teradata.Client | |![Trino](https://img.shields.io/badge/Trino-DD00A1?logo=trino&logoColor=ffffff&style=flat-square) | tr, trino | NReco.PrestoAdo | @@ -210,3 +211,8 @@ Install-Package DubUrl.Adomd Check the [first steps guide](https://seddryck.github.io/DubUrl/docs/basics-connection-url/) on the website. Please note that `DubUrl` does not install actual drivers, and only provides a standard way to [`Parse`] respective database connection URLs then [`Connect`] or [`Open`] connections. + + + + + diff --git a/appveyor.yml b/appveyor.yml index 57c7900d..e57a5303 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -17,6 +17,8 @@ skip_commits: environment: github_access_token: secure: gtEHCUmmDjYfrp/NEe2qUPjZi9gXoSgzYTNEmN99fCWp7qp4tXhVSPhzutj2WATjNqlFChFx1vcvFZtiq9C9vVjJ/lcmp/b0paF+W9V0xqScKMrkYQmCrA/glUq4XOae + SINGLESTORE_LICENSE: + secure: CnPDQsQ/dDximcta5iMng/QKahXstSQpENsXCHiDd9JbPfkYTRn8GgXsAcYQgrHKtnsA+FDAO85fxfHXyb2tmrLjJupAMKA14t58y7D0ePpeIs6ZRGu8MCQkSLk9cuiGFQeRSOxoH9gUkVPQU30M3n9+HY8gVvasxanUsWH+HWhwrl9+4gr7D8SGx4ZB4rUyEZ8vQGUw0uHerbaImkQk9Q== init: - cmd: git config --global core.autocrlf true @@ -68,7 +70,7 @@ test_script: - pwsh: | $force = ($env:APPVEYOR_REPO_BRANCH -eq "main") #Valid for a Pull Request or a Commit on main - & .\DubUrl.QA\deploy-test-harness.ps1 -force:$force -config "Release" -frameworks @("net6.0") -exclude @("CockRoach", "Drill", "Trino", "PowerBIDesktop") + & .\DubUrl.QA\deploy-test-harness.ps1 -force:$force -config "Release" -frameworks @("net6.0") -exclude @("CockRoach", "Drill", "Trino", "PowerBIDesktop", "SsasMultdim", "SsasTabular", "SingleStore") if ($lastexitcode -gt 0) { throw "At least one of the test-suite was not successful. Build stopped." } diff --git a/docs/_data/natives.json b/docs/_data/natives.json index 0e07ea69..430da574 100644 --- a/docs/_data/natives.json +++ b/docs/_data/natives.json @@ -127,6 +127,18 @@ "MainColor": "#6933FF", "SecondaryColor": "#ffffff" }, + { + "Class": "SingleStoreMapper", + "Database": "SingleStore", + "Aliases": [ + "singlestore", + "single" + ], + "ProviderInvariantName": "SingleStoreConnector", + "Slug": "singlestore", + "MainColor": "#AA00FF", + "SecondaryColor": "#ffffff" + }, { "Class": "SnowflakeMapper", "Database": "Snowflake",