From d7457060829e897c726f5d7522f56bfac11b8e9a Mon Sep 17 00:00:00 2001 From: Kyle Date: Fri, 5 Jul 2024 22:35:45 -0400 Subject: [PATCH] feat: add FSharp database abstraction layer I don't love having separate modules for ReadOnly vs. ReadWrite connections, but F# doesn't support covariance, so this is the best I could come up with for now. --- .github/workflows/publish-release.yml | 1 + ...tion.DataAccess.Abstractions.FSharp.fsproj | 31 +++ .../ReadOnlyDb.fs | 35 +++ .../ReadWriteDb.fs | 35 +++ ...ests.DataAccess.Abstractions.FSharp.fsproj | 30 +++ .../Program.fs | 4 + .../ReadOnlyDbTests.fs | 249 ++++++++++++++++++ .../ReadWriteDbTests.fs | 249 ++++++++++++++++++ FruityFoundation.sln | 14 + 9 files changed, 648 insertions(+) create mode 100644 FruityFoundation.DataAccess.Abstractions.FSharp/FruityFoundation.DataAccess.Abstractions.FSharp.fsproj create mode 100644 FruityFoundation.DataAccess.Abstractions.FSharp/ReadOnlyDb.fs create mode 100644 FruityFoundation.DataAccess.Abstractions.FSharp/ReadWriteDb.fs create mode 100644 FruityFoundation.Tests.DataAccess.Abstractions.FSharp/FruityFoundation.Tests.DataAccess.Abstractions.FSharp.fsproj create mode 100644 FruityFoundation.Tests.DataAccess.Abstractions.FSharp/Program.fs create mode 100644 FruityFoundation.Tests.DataAccess.Abstractions.FSharp/ReadOnlyDbTests.fs create mode 100644 FruityFoundation.Tests.DataAccess.Abstractions.FSharp/ReadWriteDbTests.fs diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index b66238d..b82f24f 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -46,4 +46,5 @@ jobs: dotnet nuget push "FruityFoundation.FsBase.${{ steps.generate-version.outputs.RELEASE_VERSION_NUMBER }}.nupkg" --api-key=${{ secrets.NUGET_API_KEY }} --source=https://api.nuget.org/v3/index.json --skip-duplicate dotnet nuget push "FruityFoundation.Db.${{ steps.generate-version.outputs.RELEASE_VERSION_NUMBER }}.nupkg" --api-key=${{ secrets.NUGET_API_KEY }} --source=https://api.nuget.org/v3/index.json --skip-duplicate dotnet nuget push "FruityFoundation.DataAccess.Abstractions.${{ steps.generate-version.outputs.RELEASE_VERSION_NUMBER }}.nupkg" --api-key=${{ secrets.NUGET_API_KEY }} --source=https://api.nuget.org/v3/index.json --skip-duplicate + dotnet nuget push "FruityFoundation.DataAccess.Abstractions.FSharp.${{ steps.generate-version.outputs.RELEASE_VERSION_NUMBER }}.nupkg" --api-key=${{ secrets.NUGET_API_KEY }} --source=https://api.nuget.org/v3/index.json --skip-duplicate dotnet nuget push "FruityFoundation.DataAccess.Core.${{ steps.generate-version.outputs.RELEASE_VERSION_NUMBER }}.nupkg" --api-key=${{ secrets.NUGET_API_KEY }} --source=https://api.nuget.org/v3/index.json --skip-duplicate diff --git a/FruityFoundation.DataAccess.Abstractions.FSharp/FruityFoundation.DataAccess.Abstractions.FSharp.fsproj b/FruityFoundation.DataAccess.Abstractions.FSharp/FruityFoundation.DataAccess.Abstractions.FSharp.fsproj new file mode 100644 index 0000000..a2f32f6 --- /dev/null +++ b/FruityFoundation.DataAccess.Abstractions.FSharp/FruityFoundation.DataAccess.Abstractions.FSharp.fsproj @@ -0,0 +1,31 @@ + + + + net8.0 + true + true + 1.11.0 + Kyle Ratti + https://github.com/kyleratti/FruityFoundation + true + LICENSE + + + + + + + + + + + + + + + + + + + + diff --git a/FruityFoundation.DataAccess.Abstractions.FSharp/ReadOnlyDb.fs b/FruityFoundation.DataAccess.Abstractions.FSharp/ReadOnlyDb.fs new file mode 100644 index 0000000..34f3560 --- /dev/null +++ b/FruityFoundation.DataAccess.Abstractions.FSharp/ReadOnlyDb.fs @@ -0,0 +1,35 @@ +[] +module FruityFoundation.DataAccess.Abstractions.FSharp.ReadOnlyDb + +open System.Collections.Generic +open System.Threading +open FSharp.Control +open FruityFoundation.DataAccess.Abstractions + +let private toKeyValuePair (parms : (string * obj) seq) = + parms + |> Seq.map (fun (name, value) -> KeyValuePair(name, value)) + +let query<'a> (connection : IDatabaseConnection) (cancellationToken : CancellationToken) (sql : string) (parms : (string * obj) seq) = task { + return! connection.Query<'a>(sql, parms |> toKeyValuePair, cancellationToken) +} + +let queryUnbuffered<'a> (connection : IDatabaseConnection) (cancellationToken : CancellationToken) (sql : string) (parms : (string * obj) seq) = taskSeq { + yield! connection.QueryUnbuffered<'a>(sql, parms |> toKeyValuePair, cancellationToken) +} + +let querySingle<'a> (connection : IDatabaseConnection) (cancellationToken : CancellationToken) (sql : string) (parms : (string * obj) seq) = task { + return! connection.QuerySingle<'a>(sql, parms |> toKeyValuePair, cancellationToken) +} + +let execute (connection : IDatabaseConnection) (cancellationToken : CancellationToken) (sql : string) (parms : (string * obj) seq) = task { + return! connection.Execute(sql, parms |> toKeyValuePair, cancellationToken) +} + +let executeScalar<'a> (connection : IDatabaseConnection) (cancellationToken : CancellationToken) (sql : string) (parms : (string * obj) seq) = task { + return! connection.ExecuteScalar<'a>(sql, parms |> toKeyValuePair, cancellationToken) +} + +let executeReader (connection : IDatabaseConnection) (cancellationToken : CancellationToken) (sql : string) (parms : (string * obj) seq) = task { + return! connection.ExecuteReader(sql, parms |> toKeyValuePair, cancellationToken) +} diff --git a/FruityFoundation.DataAccess.Abstractions.FSharp/ReadWriteDb.fs b/FruityFoundation.DataAccess.Abstractions.FSharp/ReadWriteDb.fs new file mode 100644 index 0000000..b6309e4 --- /dev/null +++ b/FruityFoundation.DataAccess.Abstractions.FSharp/ReadWriteDb.fs @@ -0,0 +1,35 @@ +[] +module FruityFoundation.DataAccess.Abstractions.FSharp.ReadWriteDb + +open System.Collections.Generic +open System.Threading +open FSharp.Control +open FruityFoundation.DataAccess.Abstractions + +let private toKeyValuePair (parms : (string * obj) seq) = + parms + |> Seq.map (fun (name, value) -> KeyValuePair(name, value)) + +let query<'a> (connection : IDatabaseConnection) (cancellationToken : CancellationToken) (sql : string) (parms : (string * obj) seq) = task { + return! connection.Query<'a>(sql, parms |> toKeyValuePair, cancellationToken) +} + +let queryUnbuffered<'a> (connection : IDatabaseConnection) (cancellationToken : CancellationToken) (sql : string) (parms : (string * obj) seq) = taskSeq { + yield! connection.QueryUnbuffered<'a>(sql, parms |> toKeyValuePair, cancellationToken) +} + +let querySingle<'a> (connection : IDatabaseConnection) (cancellationToken : CancellationToken) (sql : string) (parms : (string * obj) seq) = task { + return! connection.QuerySingle<'a>(sql, parms |> toKeyValuePair, cancellationToken) +} + +let execute (connection : IDatabaseConnection) (cancellationToken : CancellationToken) (sql : string) (parms : (string * obj) seq) = task { + return! connection.Execute(sql, parms |> toKeyValuePair, cancellationToken) +} + +let executeScalar<'a> (connection : IDatabaseConnection) (cancellationToken : CancellationToken) (sql : string) (parms : (string * obj) seq) = task { + return! connection.ExecuteScalar<'a>(sql, parms |> toKeyValuePair, cancellationToken) +} + +let executeReader (connection : IDatabaseConnection) (cancellationToken : CancellationToken) (sql : string) (parms : (string * obj) seq) = task { + return! connection.ExecuteReader(sql, parms |> toKeyValuePair, cancellationToken) +} diff --git a/FruityFoundation.Tests.DataAccess.Abstractions.FSharp/FruityFoundation.Tests.DataAccess.Abstractions.FSharp.fsproj b/FruityFoundation.Tests.DataAccess.Abstractions.FSharp/FruityFoundation.Tests.DataAccess.Abstractions.FSharp.fsproj new file mode 100644 index 0000000..a9ce4ad --- /dev/null +++ b/FruityFoundation.Tests.DataAccess.Abstractions.FSharp/FruityFoundation.Tests.DataAccess.Abstractions.FSharp.fsproj @@ -0,0 +1,30 @@ + + + + net8.0 + + false + false + true + + + + + + + + + + + + + + + + + + + + + + diff --git a/FruityFoundation.Tests.DataAccess.Abstractions.FSharp/Program.fs b/FruityFoundation.Tests.DataAccess.Abstractions.FSharp/Program.fs new file mode 100644 index 0000000..176a7b6 --- /dev/null +++ b/FruityFoundation.Tests.DataAccess.Abstractions.FSharp/Program.fs @@ -0,0 +1,4 @@ +module Program = + + [] + let main _ = 0 diff --git a/FruityFoundation.Tests.DataAccess.Abstractions.FSharp/ReadOnlyDbTests.fs b/FruityFoundation.Tests.DataAccess.Abstractions.FSharp/ReadOnlyDbTests.fs new file mode 100644 index 0000000..c92a6cf --- /dev/null +++ b/FruityFoundation.Tests.DataAccess.Abstractions.FSharp/ReadOnlyDbTests.fs @@ -0,0 +1,249 @@ +module FruityFoundation.Tests.DataAccess.Abstractions.FSharp.ReadOnlyDbTests + +open System.Collections.Generic +open System.Threading +open FSharp.Control +open FakeItEasy +open FruityFoundation.DataAccess.Abstractions +open FruityFoundation.DataAccess.Abstractions.FSharp +open NUnit.Framework + +let fakeDbConnection : IDatabaseConnection = A.Fake> () + +[] +let Db_Query_Calls_IDatabaseConnection_Query_NoParms () = task { + // Arrange + let sql = "SELECT 1 FROM table" + let parms = Array.empty + + // Act + let! _ = task { + return! (sql, parms) ||> ReadOnlyDb.query fakeDbConnection CancellationToken.None + } + + // Assert + A.CallTo(fun () -> fakeDbConnection.Query( + "SELECT 1 FROM table", + A>>.That.Matches(Seq.isEmpty), + CancellationToken.None)).MustHaveHappenedOnceExactly () + |> ignore +} + +[] +let Db_Query_Calls_IDatabaseConnection_Query_WithParms () = task { + // Arrange + let sql = "SELECT 1 FROM table" + let parms = [| ("@id", box 1) |] + + // Act + let! result = task { + return! (sql, parms) ||> ReadOnlyDb.query fakeDbConnection CancellationToken.None + } + + // Assert + A.CallTo(fun () -> fakeDbConnection.Query( + "SELECT 1 FROM table", + A>>.That.Matches(fun x -> + Seq.length x = 1 && Seq.head x = KeyValuePair("@id", box 1)), + CancellationToken.None)).MustHaveHappenedOnceExactly () + |> ignore +} + +[] +let Db_QueryUnbuffered_Calls_IDatabaseConnection_Query_NoParms () = task { + // Arrange + let sql = "SELECT 1 FROM table" + let parms = Array.empty + + // Act + let! _ = task { + return! (sql, parms) + ||> ReadOnlyDb.queryUnbuffered fakeDbConnection CancellationToken.None + |> TaskSeq.toArrayAsync + } + + // Assert + A.CallTo(fun () -> fakeDbConnection.QueryUnbuffered( + "SELECT 1 FROM table", + A>>.That.Matches(Seq.isEmpty), + CancellationToken.None)).MustHaveHappenedOnceExactly () + |> ignore +} + +[] +let Db_QueryUnbuffered_Calls_IDatabaseConnection_Query_WithParms () = task { + // Arrange + let sql = "SELECT 1 FROM table" + let parms = [| ("@id", box 1) |] + + // Act + let! result = task { + return! (sql, parms) + ||> ReadOnlyDb.queryUnbuffered fakeDbConnection CancellationToken.None + |> TaskSeq.toArrayAsync + } + + // Assert + A.CallTo(fun () -> fakeDbConnection.QueryUnbuffered( + "SELECT 1 FROM table", + A>>.That.Matches(fun x -> + Seq.length x = 1 && Seq.head x = KeyValuePair("@id", box 1)), + CancellationToken.None)).MustHaveHappenedOnceExactly () + |> ignore +} + +[] +let Db_QuerySingle_Calls_IDatabaseConnection_QuerySingle_NoParms () = task { + // Arrange + let sql = "SELECT 1 FROM table" + let parms = Array.empty + + // Act + let! _ = task { + return! (sql, parms) ||> ReadOnlyDb.querySingle fakeDbConnection CancellationToken.None + } + + // Assert + A.CallTo(fun () -> fakeDbConnection.QuerySingle( + "SELECT 1 FROM table", + A>>.That.Matches(Seq.isEmpty), + CancellationToken.None)).MustHaveHappenedOnceExactly () + |> ignore +} + +[] +let Db_QuerySingle_Calls_IDatabaseConnection_QuerySingle_WithParms () = task { + // Arrange + let sql = "SELECT 1 FROM table" + let parms = [| ("@id", box 1) |] + + // Act + let! result = task { + return! (sql, parms) ||> ReadOnlyDb.querySingle fakeDbConnection CancellationToken.None + } + + // Assert + A.CallTo(fun () -> fakeDbConnection.QuerySingle( + "SELECT 1 FROM table", + A>>.That.Matches(fun x -> + Seq.length x = 1 && Seq.head x = KeyValuePair("@id", box 1)), + CancellationToken.None)).MustHaveHappenedOnceExactly () + |> ignore +} + +[] +let Db_Execute_Calls_IDatabaseConnection_Execute_NoParms () = task { + // Arrange + let sql = "SELECT 1 FROM table" + let parms = Array.empty + + // Act + let! _ = task { + return! (sql, parms) ||> ReadOnlyDb.execute fakeDbConnection CancellationToken.None + } + + // Assert + A.CallTo(fun () -> fakeDbConnection.Execute( + "SELECT 1 FROM table", + A>>.That.Matches(Seq.isEmpty), + CancellationToken.None)).MustHaveHappenedOnceExactly () + |> ignore +} + +[] +let Db_Execute_Calls_IDatabaseConnection_Execute_WithParms () = task { + // Arrange + let sql = "SELECT 1 FROM table" + let parms = [| ("@id", box 1) |] + + // Act + let! result = task { + return! (sql, parms) ||> ReadOnlyDb.execute fakeDbConnection CancellationToken.None + } + + // Assert + A.CallTo(fun () -> fakeDbConnection.Execute( + "SELECT 1 FROM table", + A>>.That.Matches(fun x -> + Seq.length x = 1 && Seq.head x = KeyValuePair("@id", box 1)), + CancellationToken.None)).MustHaveHappenedOnceExactly () + |> ignore +} + +[] +let Db_ExecuteScalar_Calls_IDatabaseConnection_ExecuteScalar_NoParms () = task { + // Arrange + let sql = "SELECT 1 FROM table" + let parms = Array.empty + + // Act + let! _ = task { + return! (sql, parms) ||> ReadOnlyDb.executeScalar fakeDbConnection CancellationToken.None + } + + // Assert + A.CallTo(fun () -> fakeDbConnection.ExecuteScalar( + "SELECT 1 FROM table", + A>>.That.Matches(Seq.isEmpty), + CancellationToken.None)).MustHaveHappenedOnceExactly () + |> ignore +} + +[] +let Db_ExecuteScalar_Calls_IDatabaseConnection_ExecuteScalar_WithParms () = task { + // Arrange + let sql = "SELECT 1 FROM table" + let parms = [| ("@id", box 1) |] + + // Act + let! result = task { + return! (sql, parms) ||> ReadOnlyDb.executeScalar fakeDbConnection CancellationToken.None + } + + // Assert + A.CallTo(fun () -> fakeDbConnection.ExecuteScalar( + "SELECT 1 FROM table", + A>>.That.Matches(fun x -> + Seq.length x = 1 && Seq.head x = KeyValuePair("@id", box 1)), + CancellationToken.None)).MustHaveHappenedOnceExactly () + |> ignore +} + +[] +let Db_ExecuteReader_Calls_IDatabaseConnection_ExecuteReader_NoParms () = task { + // Arrange + let sql = "SELECT 1 FROM table" + let parms = Array.empty + + // Act + let! _ = task { + return! (sql, parms) ||> ReadOnlyDb.executeReader fakeDbConnection CancellationToken.None + } + + // Assert + A.CallTo(fun () -> fakeDbConnection.ExecuteReader( + "SELECT 1 FROM table", + A>>.That.Matches(Seq.isEmpty), + CancellationToken.None)).MustHaveHappenedOnceExactly () + |> ignore +} + +[] +let Db_ExecuteReader_Calls_IDatabaseConnection_ExecuteReader_WithParms () = task { + // Arrange + let sql = "SELECT 1 FROM table" + let parms = [| ("@id", box 1) |] + + // Act + let! result = task { + return! (sql, parms) ||> ReadOnlyDb.executeReader fakeDbConnection CancellationToken.None + } + + // Assert + A.CallTo(fun () -> fakeDbConnection.ExecuteReader( + "SELECT 1 FROM table", + A>>.That.Matches(fun x -> + Seq.length x = 1 && Seq.head x = KeyValuePair("@id", box 1)), + CancellationToken.None)).MustHaveHappenedOnceExactly () + |> ignore +} diff --git a/FruityFoundation.Tests.DataAccess.Abstractions.FSharp/ReadWriteDbTests.fs b/FruityFoundation.Tests.DataAccess.Abstractions.FSharp/ReadWriteDbTests.fs new file mode 100644 index 0000000..4faefe6 --- /dev/null +++ b/FruityFoundation.Tests.DataAccess.Abstractions.FSharp/ReadWriteDbTests.fs @@ -0,0 +1,249 @@ +module FruityFoundation.Tests.DataAccess.Abstractions.FSharp.ReadWriteDbTests + +open System.Collections.Generic +open System.Threading +open FSharp.Control +open FakeItEasy +open FruityFoundation.DataAccess.Abstractions +open FruityFoundation.DataAccess.Abstractions.FSharp +open NUnit.Framework + +let fakeDbConnection : IDatabaseConnection = A.Fake> () + +[] +let Db_Query_Calls_IDatabaseConnection_Query_NoParms () = task { + // Arrange + let sql = "SELECT 1 FROM table" + let parms = Array.empty + + // Act + let! _ = task { + return! (sql, parms) ||> ReadWriteDb.query fakeDbConnection CancellationToken.None + } + + // Assert + A.CallTo(fun () -> fakeDbConnection.Query( + "SELECT 1 FROM table", + A>>.That.Matches(Seq.isEmpty), + CancellationToken.None)).MustHaveHappenedOnceExactly () + |> ignore +} + +[] +let Db_Query_Calls_IDatabaseConnection_Query_WithParms () = task { + // Arrange + let sql = "SELECT 1 FROM table" + let parms = [| ("@id", box 1) |] + + // Act + let! result = task { + return! (sql, parms) ||> ReadWriteDb.query fakeDbConnection CancellationToken.None + } + + // Assert + A.CallTo(fun () -> fakeDbConnection.Query( + "SELECT 1 FROM table", + A>>.That.Matches(fun x -> + Seq.length x = 1 && Seq.head x = KeyValuePair("@id", box 1)), + CancellationToken.None)).MustHaveHappenedOnceExactly () + |> ignore +} + +[] +let Db_QueryUnbuffered_Calls_IDatabaseConnection_Query_NoParms () = task { + // Arrange + let sql = "SELECT 1 FROM table" + let parms = Array.empty + + // Act + let! _ = task { + return! (sql, parms) + ||> ReadWriteDb.queryUnbuffered fakeDbConnection CancellationToken.None + |> TaskSeq.toArrayAsync + } + + // Assert + A.CallTo(fun () -> fakeDbConnection.QueryUnbuffered( + "SELECT 1 FROM table", + A>>.That.Matches(Seq.isEmpty), + CancellationToken.None)).MustHaveHappenedOnceExactly () + |> ignore +} + +[] +let Db_QueryUnbuffered_Calls_IDatabaseConnection_Query_WithParms () = task { + // Arrange + let sql = "SELECT 1 FROM table" + let parms = [| ("@id", box 1) |] + + // Act + let! result = task { + return! (sql, parms) + ||> ReadWriteDb.queryUnbuffered fakeDbConnection CancellationToken.None + |> TaskSeq.toArrayAsync + } + + // Assert + A.CallTo(fun () -> fakeDbConnection.QueryUnbuffered( + "SELECT 1 FROM table", + A>>.That.Matches(fun x -> + Seq.length x = 1 && Seq.head x = KeyValuePair("@id", box 1)), + CancellationToken.None)).MustHaveHappenedOnceExactly () + |> ignore +} + +[] +let Db_QuerySingle_Calls_IDatabaseConnection_QuerySingle_NoParms () = task { + // Arrange + let sql = "SELECT 1 FROM table" + let parms = Array.empty + + // Act + let! _ = task { + return! (sql, parms) ||> ReadWriteDb.querySingle fakeDbConnection CancellationToken.None + } + + // Assert + A.CallTo(fun () -> fakeDbConnection.QuerySingle( + "SELECT 1 FROM table", + A>>.That.Matches(Seq.isEmpty), + CancellationToken.None)).MustHaveHappenedOnceExactly () + |> ignore +} + +[] +let Db_QuerySingle_Calls_IDatabaseConnection_QuerySingle_WithParms () = task { + // Arrange + let sql = "SELECT 1 FROM table" + let parms = [| ("@id", box 1) |] + + // Act + let! result = task { + return! (sql, parms) ||> ReadWriteDb.querySingle fakeDbConnection CancellationToken.None + } + + // Assert + A.CallTo(fun () -> fakeDbConnection.QuerySingle( + "SELECT 1 FROM table", + A>>.That.Matches(fun x -> + Seq.length x = 1 && Seq.head x = KeyValuePair("@id", box 1)), + CancellationToken.None)).MustHaveHappenedOnceExactly () + |> ignore +} + +[] +let Db_Execute_Calls_IDatabaseConnection_Execute_NoParms () = task { + // Arrange + let sql = "SELECT 1 FROM table" + let parms = Array.empty + + // Act + let! _ = task { + return! (sql, parms) ||> ReadWriteDb.execute fakeDbConnection CancellationToken.None + } + + // Assert + A.CallTo(fun () -> fakeDbConnection.Execute( + "SELECT 1 FROM table", + A>>.That.Matches(Seq.isEmpty), + CancellationToken.None)).MustHaveHappenedOnceExactly () + |> ignore +} + +[] +let Db_Execute_Calls_IDatabaseConnection_Execute_WithParms () = task { + // Arrange + let sql = "SELECT 1 FROM table" + let parms = [| ("@id", box 1) |] + + // Act + let! result = task { + return! (sql, parms) ||> ReadWriteDb.execute fakeDbConnection CancellationToken.None + } + + // Assert + A.CallTo(fun () -> fakeDbConnection.Execute( + "SELECT 1 FROM table", + A>>.That.Matches(fun x -> + Seq.length x = 1 && Seq.head x = KeyValuePair("@id", box 1)), + CancellationToken.None)).MustHaveHappenedOnceExactly () + |> ignore +} + +[] +let Db_ExecuteScalar_Calls_IDatabaseConnection_ExecuteScalar_NoParms () = task { + // Arrange + let sql = "SELECT 1 FROM table" + let parms = Array.empty + + // Act + let! _ = task { + return! (sql, parms) ||> ReadWriteDb.executeScalar fakeDbConnection CancellationToken.None + } + + // Assert + A.CallTo(fun () -> fakeDbConnection.ExecuteScalar( + "SELECT 1 FROM table", + A>>.That.Matches(Seq.isEmpty), + CancellationToken.None)).MustHaveHappenedOnceExactly () + |> ignore +} + +[] +let Db_ExecuteScalar_Calls_IDatabaseConnection_ExecuteScalar_WithParms () = task { + // Arrange + let sql = "SELECT 1 FROM table" + let parms = [| ("@id", box 1) |] + + // Act + let! result = task { + return! (sql, parms) ||> ReadWriteDb.executeScalar fakeDbConnection CancellationToken.None + } + + // Assert + A.CallTo(fun () -> fakeDbConnection.ExecuteScalar( + "SELECT 1 FROM table", + A>>.That.Matches(fun x -> + Seq.length x = 1 && Seq.head x = KeyValuePair("@id", box 1)), + CancellationToken.None)).MustHaveHappenedOnceExactly () + |> ignore +} + +[] +let Db_ExecuteReader_Calls_IDatabaseConnection_ExecuteReader_NoParms () = task { + // Arrange + let sql = "SELECT 1 FROM table" + let parms = Array.empty + + // Act + let! _ = task { + return! (sql, parms) ||> ReadWriteDb.executeReader fakeDbConnection CancellationToken.None + } + + // Assert + A.CallTo(fun () -> fakeDbConnection.ExecuteReader( + "SELECT 1 FROM table", + A>>.That.Matches(Seq.isEmpty), + CancellationToken.None)).MustHaveHappenedOnceExactly () + |> ignore +} + +[] +let Db_ExecuteReader_Calls_IDatabaseConnection_ExecuteReader_WithParms () = task { + // Arrange + let sql = "SELECT 1 FROM table" + let parms = [| ("@id", box 1) |] + + // Act + let! result = task { + return! (sql, parms) ||> ReadWriteDb.executeReader fakeDbConnection CancellationToken.None + } + + // Assert + A.CallTo(fun () -> fakeDbConnection.ExecuteReader( + "SELECT 1 FROM table", + A>>.That.Matches(fun x -> + Seq.length x = 1 && Seq.head x = KeyValuePair("@id", box 1)), + CancellationToken.None)).MustHaveHappenedOnceExactly () + |> ignore +} diff --git a/FruityFoundation.sln b/FruityFoundation.sln index 7b46a1c..3130355 100644 --- a/FruityFoundation.sln +++ b/FruityFoundation.sln @@ -20,6 +20,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FruityFoundation.DataAccess EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FruityFoundation.DataAccess.Core", "FruityFoundation.DataAccess.Core\FruityFoundation.DataAccess.Core.csproj", "{B65527CC-218A-4EA3-93DC-985713B5DFF4}" EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FruityFoundation.DataAccess.Abstractions.FSharp", "FruityFoundation.DataAccess.Abstractions.FSharp\FruityFoundation.DataAccess.Abstractions.FSharp.fsproj", "{B2E0156B-B631-4C80-A129-59472D2D0A77}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FruityFoundation.Tests.DataAccess.Abstractions.FSharp", "FruityFoundation.Tests.DataAccess.Abstractions.FSharp\FruityFoundation.Tests.DataAccess.Abstractions.FSharp.fsproj", "{27F4FB64-7A51-4315-BDAA-6EE07736C976}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -58,6 +62,14 @@ Global {B65527CC-218A-4EA3-93DC-985713B5DFF4}.Debug|Any CPU.Build.0 = Debug|Any CPU {B65527CC-218A-4EA3-93DC-985713B5DFF4}.Release|Any CPU.ActiveCfg = Release|Any CPU {B65527CC-218A-4EA3-93DC-985713B5DFF4}.Release|Any CPU.Build.0 = Release|Any CPU + {B2E0156B-B631-4C80-A129-59472D2D0A77}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2E0156B-B631-4C80-A129-59472D2D0A77}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2E0156B-B631-4C80-A129-59472D2D0A77}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2E0156B-B631-4C80-A129-59472D2D0A77}.Release|Any CPU.Build.0 = Release|Any CPU + {27F4FB64-7A51-4315-BDAA-6EE07736C976}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {27F4FB64-7A51-4315-BDAA-6EE07736C976}.Debug|Any CPU.Build.0 = Debug|Any CPU + {27F4FB64-7A51-4315-BDAA-6EE07736C976}.Release|Any CPU.ActiveCfg = Release|Any CPU + {27F4FB64-7A51-4315-BDAA-6EE07736C976}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {50A75644-A1C3-4495-9DEB-DBB12D9334B5} = {B44178DF-5B81-4029-90FA-2BF8E2A1EDBF} @@ -65,5 +77,7 @@ Global {A64E73D3-EF87-4938-B01E-F9CC0B59F9DE} = {B44178DF-5B81-4029-90FA-2BF8E2A1EDBF} {C003E247-C62E-4830-94E4-F274D8466A5C} = {5C3A014A-7931-4A36-95F0-5EFE15AB06A3} {B65527CC-218A-4EA3-93DC-985713B5DFF4} = {5C3A014A-7931-4A36-95F0-5EFE15AB06A3} + {B2E0156B-B631-4C80-A129-59472D2D0A77} = {5C3A014A-7931-4A36-95F0-5EFE15AB06A3} + {27F4FB64-7A51-4315-BDAA-6EE07736C976} = {B44178DF-5B81-4029-90FA-2BF8E2A1EDBF} EndGlobalSection EndGlobal