diff --git a/DubUrl.Core/Locating/OdbcDriver/Implementation/MsAccessDriverLocator.cs b/DubUrl.Core/Locating/OdbcDriver/Implementation/MsAccessDriverLocator.cs new file mode 100644 index 00000000..401f585b --- /dev/null +++ b/DubUrl.Core/Locating/OdbcDriver/Implementation/MsAccessDriverLocator.cs @@ -0,0 +1,40 @@ +using DubUrl.Locating.RegexUtils; +using DubUrl.Mapping.Database; +using DubUrl.Mapping.Implementation; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace DubUrl.Locating.OdbcDriver; + +[Driver()] +public class MsAccessDriverLocator : BaseDriverLocator +{ + internal class MsAccessDriverRegex : BaseDriverRegex + { + public MsAccessDriverRegex() + : base( + [ + new WordMatch("Microsoft Access Driver"), + new SpaceMatch(), + new LiteralMatch("(*.mdb, *.accdb)"), + ]) + { } + } + private List Candidates { get; } = []; + + public MsAccessDriverLocator() + : base(GetRegexPattern()) { } + internal MsAccessDriverLocator(DriverLister driverLister) + : base(GetRegexPattern(), driverLister) { } + + + protected override void AddCandidate(string driver, string[] matches) + => Candidates.Add(driver); + + protected override List RankCandidates() + => Candidates; +} diff --git a/DubUrl.Core/Mapping/Database/MsAccessDatabase.cs b/DubUrl.Core/Mapping/Database/MsAccessDatabase.cs new file mode 100644 index 00000000..a8284b27 --- /dev/null +++ b/DubUrl.Core/Mapping/Database/MsAccessDatabase.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( + "Microsoft Access" + , ["accdb", "access", "msaccess"] + , DatabaseCategory.FileBased +)] +[Brand("microsoftaccess", "#217346")] +public class MsAccessDatabase : IDatabase +{ } diff --git a/DubUrl.Core/Querying/Dialects/Formatters/DateCrossSurroundingFormatter.cs b/DubUrl.Core/Querying/Dialects/Formatters/DateCrossSurroundingFormatter.cs new file mode 100644 index 00000000..cfc81117 --- /dev/null +++ b/DubUrl.Core/Querying/Dialects/Formatters/DateCrossSurroundingFormatter.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DubUrl.Querying.Dialects.Formatters; + +internal class DateCrossSurroundingFormatter : IValueFormatter +{ + public string Format(DateOnly value) + => $"#{value:MM/dd/yyyy}#"; + public string Format(object obj) + => obj is DateOnly value ? Format(value) : throw new Exception(); +} diff --git a/DubUrl.Core/Querying/Dialects/MsAccessDialect.cs b/DubUrl.Core/Querying/Dialects/MsAccessDialect.cs new file mode 100644 index 00000000..de7bd619 --- /dev/null +++ b/DubUrl.Core/Querying/Dialects/MsAccessDialect.cs @@ -0,0 +1,17 @@ +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()] +[ParentLanguage] +public class MsAccessDialect : BaseDialect +{ + internal MsAccessDialect(ILanguage language, string[] aliases, IRenderer renderer, ICaster[] casters) + : base(language, aliases, renderer, casters) { } +} diff --git a/DubUrl.Core/Querying/Dialects/Renderers/MsAccessRenderer.cs b/DubUrl.Core/Querying/Dialects/Renderers/MsAccessRenderer.cs new file mode 100644 index 00000000..2dc6f625 --- /dev/null +++ b/DubUrl.Core/Querying/Dialects/Renderers/MsAccessRenderer.cs @@ -0,0 +1,20 @@ +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 MsAccessRenderer : AnsiRenderer +{ + public MsAccessRenderer() + : base(new ValueFormatter() + .With(new DateCrossSurroundingFormatter()) + , new NullFormatter() + , new IdentifierSquareBracketFormatter()) { } + + protected MsAccessRenderer(BaseValueFormatter value) + : base(value, new NullFormatter(), new IdentifierBacktickFormatter()) { } +} diff --git a/DubUrl.QA/DubUrl.QA.csproj b/DubUrl.QA/DubUrl.QA.csproj index 207d86a6..05eed1df 100644 --- a/DubUrl.QA/DubUrl.QA.csproj +++ b/DubUrl.QA/DubUrl.QA.csproj @@ -122,6 +122,9 @@ PreserveNewest + + Always + Always diff --git a/DubUrl.QA/MsAccess/DubUrl.accdb b/DubUrl.QA/MsAccess/DubUrl.accdb new file mode 100644 index 00000000..b0ba8ebd Binary files /dev/null and b/DubUrl.QA/MsAccess/DubUrl.accdb differ diff --git a/DubUrl.QA/MsAccess/OdbcDriverMsAccess.cs b/DubUrl.QA/MsAccess/OdbcDriverMsAccess.cs new file mode 100644 index 00000000..4edba9b2 --- /dev/null +++ b/DubUrl.QA/MsAccess/OdbcDriverMsAccess.cs @@ -0,0 +1,28 @@ +using NUnit.Framework; +using System.Diagnostics; +using System.Data; +using System.Data.Common; +using DubUrl.Registering; +using System.Data.Odbc; +using System.Drawing; + +namespace DubUrl.QA.MsAccess; + +[Category("MsAccess")] +[FixtureLifeCycle(LifeCycle.SingleInstance)] +public class OdbcDriverMsAccess : BaseOdbcDriver +{ + protected string CurrentDirectory + => Path.GetDirectoryName(GetType().Assembly.Location) + "\\"; + + public override string ConnectionString + => $"odbc+accdb:///MsAccess/DubUrl.accdb?Defaultdir={CurrentDirectory}"; + + [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/MsAccess/deploy-msaccess-test-env.ps1 b/DubUrl.QA/MsAccess/deploy-msaccess-test-env.ps1 new file mode 100644 index 00000000..099812ce --- /dev/null +++ b/DubUrl.QA/MsAccess/deploy-msaccess-test-env.ps1 @@ -0,0 +1,42 @@ +Param( + [switch] $force=$false + , [string] $config = "Release" + , [string[]] $frameworks = @("net6.0", "net7.0") +) +. $PSScriptRoot\..\Run-TestSuite.ps1 + +if ($force) { + Write-Host "Enforcing QA testing for Microsoft Access" +} + +$filesChanged = & git diff --name-only HEAD HEAD~1 +if ($force -or ($filesChanged -like "*access*")) { + Write-Host "Deploying Microsoft Access testing environment" + + # Installing ODBC driver + Write-host "`tDeploying Microsoft Access ODBC drivers" + $drivers = Get-OdbcDriver -Name "*accdb*" -Platform "64-bit" + + If ($drivers.Length -eq 0) { + Write-Host "`t`tDownloading Microsoft Access ODBC driver ..." + Invoke-WebRequest "https://download.microsoft.com/download/3/5/C/35C84C36-661A-44E6-9324-8786B8DBE231/accessdatabaseengine_X64.exe" -OutFile "$env:temp\accessdatabaseengine_X64.exe" + Write-Host "`t`tInstalling Microsoft Access ODBC driver ..." + & cmd /c start /wait "$env:temp\accessdatabaseengine_X64.exe" /quiet + Write-Host "`t`tChecking installation ..." + Get-OdbcDriver -Name "*accdb*" + Write-Host "`tDeployment of Microsoft Access ODBC driver finalized." + } else { + Write-Host "`t`tDrivers already installed:" + Get-OdbcDriver -Name "*accdb*" -Platform "64-bit" + Write-Host "`t`tSkipping installation of new drivers" + } + + # Running QA tests + Write-Host "Running QA tests related to Microsoft Access" + $testSuccessful = Run-TestSuite @("MsAccess") -config $config -frameworks $frameworks + + # Raise failing tests + exit $testSuccessful +} else { + return -1 +} diff --git a/DubUrl.Testing/Locating/OdbcDriver/Implementation/MsAccessDriverLocatorTest.cs b/DubUrl.Testing/Locating/OdbcDriver/Implementation/MsAccessDriverLocatorTest.cs new file mode 100644 index 00000000..88954077 --- /dev/null +++ b/DubUrl.Testing/Locating/OdbcDriver/Implementation/MsAccessDriverLocatorTest.cs @@ -0,0 +1,58 @@ +using DubUrl.Locating.OdbcDriver; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DubUrl.Testing.Locating.OdbcDriver.Implementation; + +public class MsAccessDriverLocatorTest +{ + private class FakeDriverLister : DriverLister + { + private string[] Drivers { get; } + + public FakeDriverLister(string[] drivers) + => Drivers = drivers; + + public override string[] List() => Drivers; + } + + [Test] + public void Locate_SingleElementMatching_ElementReturned() + { + var driverLister = new FakeDriverLister(["Microsoft Access Driver (*.mdb, *.accdb)"]); + var driverLocator = new MsAccessDriverLocator(driverLister); + var driver = driverLocator.Locate(); + Assert.That(driver, Is.EqualTo("Microsoft Access Driver (*.mdb, *.accdb)")); + } + + [Test] + public void Locate_MultipleIdenticalElementMatching_BestElementReturned() + { + var driverLister = new FakeDriverLister(["Microsoft Access Driver (*.mdb, *.accdb)", "Microsoft Access Driver (*.mdb, *.accdb)"]); + var driverLocator = new MsAccessDriverLocator(driverLister); + var driver = driverLocator.Locate(); + Assert.That(driver, Is.EqualTo("Microsoft Access Driver (*.mdb, *.accdb)")); + } + + [Test] + public void Locate_ElementNonMatching_ElementNotReturned() + { + var driverLister = new FakeDriverLister(["ODBC Driver 13 for SQL Server", "Microsoft Access Driver (*.mdb, *.accdb)"]); + var driverLocator = new MsAccessDriverLocator(driverLister); + var driver = driverLocator.Locate(); + Assert.That(driver, Is.EqualTo("Microsoft Access Driver (*.mdb, *.accdb)")); + } + + [Test] + public void Locate_NoMatching_EmptyString() + { + var driverLister = new FakeDriverLister(["ODBC Driver 17 for Other Database"]); + var driverLocator = new MsAccessDriverLocator(driverLister); + var driver = driverLocator.Locate(); + Assert.That(driver, Is.Null.Or.Empty); + } +} diff --git a/DubUrl.Testing/Querying/Dialects/Formatters/FormattersTest.cs b/DubUrl.Testing/Querying/Dialects/Formatters/FormattersTest.cs index d1ec3501..52f5e1fb 100644 --- a/DubUrl.Testing/Querying/Dialects/Formatters/FormattersTest.cs +++ b/DubUrl.Testing/Querying/Dialects/Formatters/FormattersTest.cs @@ -57,6 +57,11 @@ public void CastFormatter_Format_Match(DateTime value, string expected) public void DateFormatter_Format_Match(string value, string expected) => Assert.That(new DateFormatter().Format(DateOnly.Parse(value)), Is.EqualTo(expected)); + [Test] + [TestCase("2023-12-16", "#12/16/2023#")] + public void DateCrossSurroundingFormatter_Format_Match(string value, string expected) + => Assert.That(new DateCrossSurroundingFormatter().Format(DateOnly.Parse(value)), Is.EqualTo(expected)); + [Test] [TestCase("17:02:46", "'17:02:46'")] [TestCase("17:02:46.128", "'17:02:46.128'")] diff --git a/README.md b/README.md index aeef46d9..172ed12d 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ DubUrl provides a standard, URL style mechanism for parsing database connection [![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 ODBC drivers implemented badge](https://img.shields.io/badge/Mappers%20for%20ODBC%20drivers-12%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) @@ -151,6 +151,7 @@ The following databases and their associated schemes are supported out of the bo |![DuckDB](https://img.shields.io/badge/DuckDB-FFF000?logo=duckdb&logoColor=000000&style=flat-square) | duck, duckdb | ^\bDuckDB\s\bDriver$ | |![Apache Drill](https://img.shields.io/badge/Apache%20Drill-333333?logo=&logoColor=ffffff&style=flat-square) | drill | ^\bMapR Drill ODBC Driver$ | |![Trino](https://img.shields.io/badge/Trino-DD00A1?logo=trino&logoColor=ffffff&style=flat-square) | tr, trino | ^(Simba)\s\bTrino ODBC Driver$ | +|![Microsoft Access](https://img.shields.io/badge/Microsoft%20Access-217346?logo=microsoftaccess&logoColor=ffffff&style=flat-square) | accdb, access, msaccess | ^\bMicrosoft Access Driver\s\(\*\.mdb, \*\.accdb\)$ | |![Microsoft Excel](https://img.shields.io/badge/Microsoft%20Excel-217346?logo=microsoftexcel&logoColor=ffffff&style=flat-square) | xls, xlsx, xlsb, xlsm | ^\bMicrosoft Excel Driver\s\(\*\.xls, \*\.xlsx, \*\.xlsm, \*\.xlsb\)$ | |![Text files](https://img.shields.io/badge/Text%20files-333333?logo=&logoColor=ffffff&style=flat-square) | txt, csv, tsv | ^\bMicrosoft Access Text Driver\s\(\*\.txt, \*\.csv\)$ | |![QuestDb](https://img.shields.io/badge/QuestDb-333333?logo=&logoColor=ffffff&style=flat-square) | quest, questdb | ^\bPostgreSQL\s(ANSI\|Unicode)(\(x64\))?$ | @@ -216,3 +217,8 @@ Please note that `DubUrl` does not install actual drivers, and only provides a s + + + + + diff --git a/docs/_data/odbc.json b/docs/_data/odbc.json index 0bcf54ae..f3c311e7 100644 --- a/docs/_data/odbc.json +++ b/docs/_data/odbc.json @@ -104,6 +104,20 @@ "MainColor": "#DD00A1", "SecondaryColor": "#ffffff" }, + { + "Class": "MsAccessDriverLocator", + "Database": "Microsoft Access", + "Aliases": [ + "accdb", + "access", + "msaccess" + ], + "NamePattern": "^\\bMicrosoft Access Driver\\s\\(\\*\\.mdb, \\*\\.accdb\\)$", + "Options": [], + "Slug": "microsoftaccess", + "MainColor": "#217346", + "SecondaryColor": "#ffffff" + }, { "Class": "MsExcelDriverLocator", "Database": "Microsoft Excel",