Skip to content

Commit

Permalink
Db.setParamsRaw, Db.execManyRaw
Browse files Browse the repository at this point in the history
  • Loading branch information
pimbrouwers committed Dec 11, 2023
1 parent 41af547 commit ba07508
Show file tree
Hide file tree
Showing 7 changed files with 273 additions and 143 deletions.
134 changes: 55 additions & 79 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,13 @@ module Author =
{ FullName = rd.ReadString "full_name" }
let authors (conn : IDbConnection) : Author list =
let sql = "
SELECT full_name
FROM author
WHERE author_id = @author_id"
let param = [ "author_id", sqlInt32 1 ]
conn
|> Db.newCommand sql
|> Db.setParams param
|> Db.newCommand "
SELECT full_name
FROM author
WHERE author_id = @author_id"
|> Db.setParams [
"author_id", SqlType.Int32 1 ]
|> Db.query Author.ofDataReader
```

Expand Down Expand Up @@ -86,127 +83,101 @@ module Author -
> Important: Donald is set to use `CommandBehavior.SequentialAccess` by default. See [performance](#performance) for more information.
```fsharp
let sql = "SELECT author_id, full_name FROM author"
conn
|> Db.newCommand sql
|> Db.newCommand "SELECT author_id, full_name FROM author"
|> Db.query Author.ofDataReader // Author list
// Async
conn
|> Db.newCommand sql
|> Db.newCommand "SELECT author_id, full_name FROM author"
|> Db.Async.query Author.ofDataReader // Task<Author list>
```

### Query for a single strongly-typed result

```fsharp
let sql = "SELECT author_id, full_name FROM author"
conn
|> Db.newCommand sql
|> Db.setParams [ "author_id", sqlInt32 1 ]
|> Db.newCommand "SELECT author_id, full_name FROM author"
|> Db.setParams [ "author_id", SqlType.Int32 1 ]
|> Db.querySingle Author.ofDataReader // Author option
// Async
conn
|> Db.newCommand sql
|> Db.setParams [ "author_id", sqlInt32 1 ]
|> Db.newCommand "SELECT author_id, full_name FROM author"
|> Db.setParams [ "author_id", SqlType.Int32 1 ]
|> Db.Async.querySingle Author.ofDataReader // Task<Author option>
```

### Execute a statement

```fsharp
let sql = "INSERT INTO author (full_name)"
// Strongly typed input parameters
let param = [ "full_name", sqlString "John Doe" ]
conn
|> Db.newCommand sql
|> Db.setParams param
|> Db.newCommand "INSERT INTO author (full_name)"
|> Db.setParams [ "full_name", SqlType.String "John Doe" ]
|> Db.exec // unit
// Async
conn
|> Db.newCommand sql
|> Db.setParams param
|> Db.newCommand "INSERT INTO author (full_name)"
|> Db.setParams [ "full_name", SqlType.String "John Doe" ]
|> Db.Async.exec // Task<unit>
```

### Execute a statement many times

```fsharp
let sql = "INSERT INTO author (full_name)"
let param =
[ "full_name", sqlString "John Doe"
"full_name", sqlString "Jane Doe" ]
conn
|> Db.newCommand sql
|> Db.execMany param
|> Db.newCommand "INSERT INTO author (full_name)"
|> Db.execMany [
"full_name", SqlType.String "John Doe"
"full_name", SqlType.String "Jane Doe" ] // unit
// Async
conn
|> Db.newCommand sql
|> Db.Async.execMany param
|> Db.newCommand "INSERT INTO author (full_name)"
|> Db.Async.execMany [
"full_name", SqlType.String "John Doe"
"full_name", SqlType.String "Jane Doe" ] //Task<unit>
```

```fsharp
let sql = "INSERT INTO author (full_name)"
### Execute statements within an explicit transaction

let param = [ "full_name", sqlString "John Doe" ]
This can be accomplished in two ways:

conn
|> Db.newCommand sql
|> Db.setParams param
|> Db.exec // unit
1. Using `Db.batch` or `Db.Async.batch` which processes the action in an *all-or-none* fashion.

// Async
```fsharp
conn
|> Db.newCommand sql
|> Db.setParams param
|> Db.Async.exec // Task<unit>
|> Db.batch (fun tran ->
for fullName in [ "John Doe"; "Jane Doe" ] do
tran
|> Db.newCommandForTransaction "INSERT INTO author (full_name) VALUES (@full_name)"
|> Db.setParams ["full_name", SqlType.String fullName ]
|> Db.exec)
```

### Execute statements within an explicit transaction

Donald exposes most of it's functionality through the `Db` module. But three `IDbTransaction` type extension are exposed to make dealing with transactions safer:

- `TryBeginTransaction()` opens a new transaction or raises `DbTransactionError`
- `TryCommit()` commits a transaction or raises `DbTransactionError` and rolls back
- `TryRollback()` rolls back a transaction or raises `DbTransactionError`
2. Using the `IDbCommand` extensions, `TryBeginTransaction()`, `TryCommit()` and `TryRollback()`.

```fsharp
// Safely begin transaction or throw CouldNotBeginTransactionError on failure
use tran = conn.TryBeginTransaction()
let insertSql = "INSERT INTO author (full_name)"
let param = [ "full_name", sqlString "John Doe" ]
conn
|> Db.newCommand "INSERT INTO author (full_name)"
|> Db.setTransaction tran
|> Db.setParams [ "full_name", sqlString "John Doe" ]
|> Db.exec
let insertResult =
conn
|> Db.newCommand insertSql
|> Db.setTransaction tran
|> Db.setParams param
|> Db.exec
|> Db.newCommand "INSERT INTO author (full_name)"
|> Db.setTransaction tran
|> Db.setParams [ "full_name", sqlString "Jane Doe" ]
|> Db.exec
match insertResult with
| Ok () ->
// Attempt to commit, rollback on failure and throw CouldNotCommitTransactionError
tran.TryCommit ()
// Attempt to commit, will rollback automatically on failure, or throw DbTransactionException
tran.TryCommit ()
conn
|> Db.newCommand "SELECT author_id, full_name FROM author WHERE full_name = @full_name"
|> Db.setParams param
|> Db.querySingle Author.ofDataReader
| Error e ->
// Attempt to commit, rollback on failure and throw CouldNotCommitTransactionError
tran.TryRollback ()
Error e
// Will rollback or throw DbTransactionException
// tran.TryRollback ()
```

## Command Parameters
Expand Down Expand Up @@ -237,13 +208,13 @@ let p1 : SqlType = SqlType.Null
let p2 : SqlType = SqlType.Int32 1
```

Helpers also exist which implicitly call the respective F# conversion function. Which are especially useful when you are working with value types in your program.
Helpers also exist which implicitly call the respective F# conversion function. Which can be especially useful when you are working with value types in your program.

```fsharp
let p1 : SqlType = sqlInt32 "1" // equivalent to SqlType.Int32 (int "1")
```

> `string` is used here **only** for demonstration purposes.
###

## Reading Values

Expand Down Expand Up @@ -305,6 +276,11 @@ type DbExecutionException =
type DbReaderException =
inherit Exception
val FieldName : string option
/// Details of failure to commit or rollback an IDbTransaction
type DbTransactionException =
inherit Exception
val Step : DbTransactionStep
```

## Performance
Expand Down
4 changes: 0 additions & 4 deletions src/Donald/Core.fs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ type DbConnectionException =
new() = { inherit Exception(); ConnectionString = None }
new(message : string) = { inherit Exception(message); ConnectionString = None }
new(message : string, inner : Exception) = { inherit Exception(message, inner); ConnectionString = None }
new(info : SerializationInfo, context : StreamingContext) = { inherit Exception(info, context); ConnectionString = None }
new(connection : IDbConnection, inner : Exception) = { inherit Exception($"Failed to establish database connection: {connection.ConnectionString}", inner); ConnectionString = Some connection.ConnectionString}

/// Details the steps of database a transaction.
Expand All @@ -100,7 +99,6 @@ type DbExecutionException =
new() = { inherit Exception(); Statement = None }
new(message : string) = { inherit Exception(message); Statement = None }
new(message : string, inner : Exception) = { inherit Exception(message, inner); Statement = None }
new(info : SerializationInfo, context : StreamingContext) = { inherit Exception(info, context); Statement = None }
new(cmd : IDbCommand, inner : Exception) = { inherit Exception($"Failed to process database command:\n{cmd.ToDetailString()}", inner); Statement = Some (cmd.ToDetailString()) }

/// Details of failure to process a database transaction.
Expand All @@ -110,7 +108,6 @@ type DbTransactionException =
new() = { inherit Exception(); Step = None }
new(message : string) = { inherit Exception(message); Step = None }
new(message : string, inner : Exception) = { inherit Exception(message, inner); Step = None }
new(info : SerializationInfo, context : StreamingContext) = { inherit Exception(info, context); Step = None }
new(step : DbTransactionStep, inner : Exception) = { inherit Exception($"Failed to process transaction at step {step}", inner); Step = Some step }

/// Details of failure to access and/or cast an IDataRecord field.
Expand All @@ -120,7 +117,6 @@ type DbReaderException =
new() = { inherit Exception(); FieldName = None }
new(message : string) = { inherit Exception(message); FieldName = None }
new(message : string, inner : Exception) = { inherit Exception(message, inner); FieldName = None }
new(info : SerializationInfo, context : StreamingContext) = { inherit Exception(info, context); FieldName = None }
new(fieldName : string, inner : IndexOutOfRangeException) = { inherit Exception($"Failed to read database field: '{fieldName}'", inner); FieldName = Some fieldName }
new(fieldName : string, inner : InvalidCastException) = { inherit Exception($"Failed to read database field: '{fieldName}'", inner); FieldName = Some fieldName }

Expand Down
57 changes: 37 additions & 20 deletions src/Donald/Db.fs
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,14 @@ module Db =
dbUnit.Command.CommandType <- commandType
dbUnit

/// Configure the command parameters for the provided DbUnit
/// Configure the strongly-typed command parameters for the provided DbUnit
let setParams (param : RawDbParams) (dbUnit : DbUnit) : DbUnit =
dbUnit.Command.SetDbParams(DbParams.create param) |> ignore
dbUnit.Command.SetParams(DbParams.create param) |> ignore
dbUnit

/// Configure the command parameters for the provided DbUnit
let setParamsRaw (param : (string * obj) list) (dbUnit : DbUnit) : DbUnit =
dbUnit.Command.SetParamsRaw(param) |> ignore
dbUnit

/// Configure the timeout for the provided DbUnit
Expand Down Expand Up @@ -61,12 +66,15 @@ module Db =
let exec (dbUnit : DbUnit) : unit =
tryDo dbUnit (fun cmd -> cmd.Exec())

/// Execute parameterized query many times with no results.
/// Execute a strongly-type parameterized query many times with no results.
let execMany (param : RawDbParams list) (dbUnit : DbUnit) : unit =
dbUnit.Command.Connection.TryOpenConnection()
for p in param do
let dbParams = DbParams.create p
dbUnit.Command.SetDbParams(dbParams).Exec()
tryDo dbUnit (fun cmd ->
for p in param do cmd.SetParams(DbParams.create p).Exec())

/// Execute a parameterized query many times with no results.
let execManyRaw (param : ((string * obj) list) list) (dbUnit : DbUnit) : unit =
tryDo dbUnit (fun cmd ->
for p in param do cmd.SetParamsRaw(p).Exec())

/// Execute scalar query and box the result.
let scalar (convert : obj -> 'a) (dbUnit : DbUnit) : 'a =
Expand All @@ -90,9 +98,12 @@ module Db =

/// Execute an all or none batch of commands.
let batch (fn : IDbTransaction -> 'a) (conn : IDbConnection) =
conn.TryOpenConnection()
use tran = conn.TryBeginTransaction()
try
fn tran
let result = fn tran
tran.TryCommit()
result
with _ ->
tran.TryRollback()
reraise ()
Expand All @@ -111,14 +122,17 @@ module Db =
let! _ = cmd.ExecAsync(dbUnit.CancellationToken)
return () })

/// Asynchronously execute parameterized query many times with no results
/// Asynchronously execute a strongly-type parameterized query many times with no results.
let execMany (param : RawDbParams list) (dbUnit : DbUnit) : Task<unit> =
tryDoAsync dbUnit (fun (cmd : DbCommand) -> task {
for p in param do
let dbParams = DbParams.create p
let! _ = cmd.SetDbParams(dbParams).ExecAsync(dbUnit.CancellationToken)
()
return () })
do! cmd.SetParams(DbParams.create p).ExecAsync(dbUnit.CancellationToken) })

/// Asynchronously execute a parameterized query many times with no results.
let execManyRaw (param : ((string * obj) list) list) (dbUnit : DbUnit) : Task<unit> =
tryDoAsync dbUnit (fun cmd -> task {
for p in param do
do! cmd.SetParamsRaw(p).ExecAsync(dbUnit.CancellationToken) })

/// Execute scalar query and box the result.
let scalar (convert : obj -> 'a) (dbUnit : DbUnit) : Task<'a> =
Expand All @@ -142,10 +156,13 @@ module Db =

/// Execute an all or none batch of commands asynchronously.
let batch (fn : IDbTransaction -> Task<unit>) (conn : IDbConnection) =
task {
use! tran = conn.TryBeginTransactionAsync()
try
return! fn tran
with _ ->
do! tran.TryRollbackAsync()
}
conn.TryOpenConnection()
use tran = conn.TryBeginTransaction()
try
task {
let! result = fn tran
tran.TryCommit()
return result }
with _ ->
tran.TryRollback()
reraise()
4 changes: 2 additions & 2 deletions src/Donald/Donald.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<AssemblyName>Donald</AssemblyName>
<Version>10.0.1</Version>
<Version>10.0.2</Version>

<!-- General info -->
<Description>Functional F# interface for ADO.NET.</Description>
Expand All @@ -11,7 +11,7 @@
<NeutralLanguage>en-CA</NeutralLanguage>

<!-- Build config -->
<TargetFrameworks>net6.0</TargetFrameworks>
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
<DebugType>embedded</DebugType>
<OutputType>Library</OutputType>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
Expand Down
2 changes: 1 addition & 1 deletion src/Donald/IDataReader.fs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ module IDataReaderExtensions =
member x.ReadByteOption(name : string) = name |> x.GetOption(fun i -> x.GetByte(i))

/// Safely retrieve Char Option
member x.ReadCharOption(name : string) = name |> x.GetOption(fun i -> x.GetString(i).[0])
member x.ReadCharOption(name : string) = name |> x.GetOption(fun i -> x.GetChar(i))

/// Safely retrieve DateTime Option
member x.ReadDateTimeOption(name : string) = name |> x.GetOption(fun i -> x.GetDateTime(i))
Expand Down
Loading

0 comments on commit ba07508

Please sign in to comment.