From ba07508553edb44bafc0e6b06c9370520fa378e8 Mon Sep 17 00:00:00 2001 From: Pim Brouwers Date: Mon, 11 Dec 2023 10:45:56 -0500 Subject: [PATCH] Db.setParamsRaw, Db.execManyRaw --- README.md | 134 +++++++++++--------------- src/Donald/Core.fs | 4 - src/Donald/Db.fs | 57 +++++++---- src/Donald/Donald.fsproj | 4 +- src/Donald/IDataReader.fs | 2 +- src/Donald/IDbCommand.fs | 27 ++++-- test/Donald.Tests/Tests.fs | 188 +++++++++++++++++++++++++++++++------ 7 files changed, 273 insertions(+), 143 deletions(-) diff --git a/README.md b/README.md index 547606d..b479e64 100644 --- a/README.md +++ b/README.md @@ -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 ``` @@ -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 ``` ### 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 ``` ### 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 ``` ### 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 ``` -```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 +|> 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 @@ -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 @@ -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 diff --git a/src/Donald/Core.fs b/src/Donald/Core.fs index 42f3875..d6ec79d 100644 --- a/src/Donald/Core.fs +++ b/src/Donald/Core.fs @@ -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. @@ -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. @@ -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. @@ -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 } diff --git a/src/Donald/Db.fs b/src/Donald/Db.fs index ce163b2..2da0e97 100644 --- a/src/Donald/Db.fs +++ b/src/Donald/Db.fs @@ -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 @@ -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 = @@ -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 () @@ -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 = 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 = + 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> = @@ -142,10 +156,13 @@ module Db = /// Execute an all or none batch of commands asynchronously. let batch (fn : IDbTransaction -> Task) (conn : IDbConnection) = - task { - use! tran = conn.TryBeginTransactionAsync() - try - return! fn tran - with _ -> - do! tran.TryRollbackAsync() - } \ No newline at end of file + conn.TryOpenConnection() + use tran = conn.TryBeginTransaction() + try + task { + let! result = fn tran + tran.TryCommit() + return result } + with _ -> + tran.TryRollback() + reraise() \ No newline at end of file diff --git a/src/Donald/Donald.fsproj b/src/Donald/Donald.fsproj index 663dc7b..a8f2531 100644 --- a/src/Donald/Donald.fsproj +++ b/src/Donald/Donald.fsproj @@ -2,7 +2,7 @@ Donald - 10.0.1 + 10.0.2 Functional F# interface for ADO.NET. @@ -11,7 +11,7 @@ en-CA - net6.0 + net6.0;net7.0;net8.0 embedded Library true diff --git a/src/Donald/IDataReader.fs b/src/Donald/IDataReader.fs index b9ae169..9f0a679 100644 --- a/src/Donald/IDataReader.fs +++ b/src/Donald/IDataReader.fs @@ -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)) diff --git a/src/Donald/IDbCommand.fs b/src/Donald/IDbCommand.fs index 1a9a48c..17e6996 100644 --- a/src/Donald/IDbCommand.fs +++ b/src/Donald/IDbCommand.fs @@ -39,7 +39,18 @@ module IDbCommandExtensions = with | :? DbException as ex -> raise (DbExecutionException(x, ex)) - member internal x.SetDbParams(dbParams : DbParams) = + member internal x.SetParamsRaw(rawDbParams : (string * obj) list) = + x.Parameters.Clear() + for (name, value) in rawDbParams do + let p = x.CreateParameter() + p.ParameterName <- name + match isNull value with + | true -> p.Value <- DBNull.Value + | false -> p.Value <- value + x.Parameters.Add(p) |> ignore + x + + member internal x.SetParams(dbParams : DbParams) = let setParamValue (dbType : DbType) (p : IDbDataParameter) (v : obj) = p.DbType <- dbType if isNull v then p.Value <- DBNull.Value @@ -57,8 +68,8 @@ module IDbCommandExtensions = | SqlType.AnsiString v -> setParamValue DbType.AnsiString p v | SqlType.Boolean v -> setParamValue DbType.Boolean p v | SqlType.Byte v -> setParamValue DbType.Byte p v - | SqlType.Char v -> setParamValue DbType.AnsiString p v - | SqlType.AnsiChar v -> setParamValue DbType.String p v + | SqlType.Char v + | SqlType.AnsiChar v -> setParamValue DbType.Object p v | SqlType.Decimal v -> setParamValue DbType.Decimal p v | SqlType.Double v | SqlType.Float v -> setParamValue DbType.Double p v @@ -76,12 +87,16 @@ module IDbCommandExtensions = x type DbCommand with - member internal x.SetDbParams(param : DbParams) = - (x :> IDbCommand).SetDbParams(param) :?> DbCommand + member internal x.SetParamsRaw(rawParams : (string * obj) list) = + (x :> IDbCommand).SetParamsRaw(rawParams) :?> DbCommand + + member internal x.SetParams(param : DbParams) = + (x :> IDbCommand).SetParams(param) :?> DbCommand member internal x.ExecAsync(?ct: CancellationToken) = task { try - return! x.ExecuteNonQueryAsync(cancellationToken = defaultArg ct CancellationToken.None) + let! _ = x.ExecuteNonQueryAsync(cancellationToken = defaultArg ct CancellationToken.None) + () with | :? DbException as ex -> return raise (DbExecutionException(x, ex)) } diff --git a/test/Donald.Tests/Tests.fs b/test/Donald.Tests/Tests.fs index 90fa622..ad3ee64 100644 --- a/test/Donald.Tests/Tests.fs +++ b/test/Donald.Tests/Tests.fs @@ -11,16 +11,6 @@ open System.Threading let conn = new SQLiteConnection("Data Source=:memory:;Version=3;New=true;") -// let shouldNotBeError pred (result : Result<'a, DbError>) = -// match result with -// | Ok result' -> pred result' -// | Error e -> sprintf "DbResult should not be Error: %A" e |> should equal false - -// let shouldNotBeOk (result : Result<'a, DbError>) = -// match result with -// | Error ex -> ex |> should be instanceOfType -// | _ -> "DbResult should not be Ok" |> should equal false - type Author = { AuthorId : int FullName : string } @@ -55,7 +45,11 @@ type DbCollection () = type ExecutionTests() = [] member _.``SELECT all sql types`` () = - let sql = " + let guidParam = Guid.NewGuid () + let dateTimeParam = DateTime.Now + + conn + |> Db.newCommand " SELECT @p_null AS p_null , @p_string AS p_string , @p_ansi_string AS p_ansi_string @@ -71,31 +65,97 @@ type ExecutionTests() = , @p_int32 AS p_int32 , @p_int64 AS p_int64 , @p_date_time AS p_date_time" + |> Db.setParams [ + "p_null", SqlType.Null + "p_string", sqlString "p_string" + "p_ansi_string", SqlType.AnsiString "p_ansi_string" + "p_boolean", sqlBoolean false + "p_byte", sqlByte Byte.MinValue + "p_char", sqlChar 'a' + "p_ansi_char", SqlType.AnsiChar Char.MinValue + "p_decimal", sqlDecimal 0.0M + "p_double", sqlDouble 0.0 + "p_float", sqlFloat 0.0 + "p_guid", sqlGuid guidParam + "p_int16", sqlInt16 16s + "p_int32", sqlInt32 32 + "p_int64", sqlInt64 64L + "p_date_time", sqlDateTime dateTimeParam ] + |> Db.querySingle (fun rd -> + {| + p_null = rd.ReadString "p_null" + p_string = rd.ReadString "p_string" + p_ansi_string = rd.ReadString "p_ansi_string" + p_boolean = rd.ReadBoolean "p_boolean" + p_byte = rd.ReadByte "p_byte" + p_char = rd.ReadChar "p_char" + p_ansi_char = rd.ReadChar "p_ansi_char" + p_decimal = rd.ReadDecimal "p_decimal" + p_double = rd.ReadDouble "p_double" + p_float = rd.ReadFloat "p_float" + p_guid = rd.ReadGuid "p_guid" + p_int16 = rd.ReadInt16 "p_int16" + p_int32 = rd.ReadInt32 "p_int32" + p_int64 = rd.ReadInt64 "p_int64" + p_date_time = rd.ReadDateTime "p_date_time" + |}) + |> fun result -> + result.IsSome |> should equal true + result.Value.p_null |> should equal "" + result.Value.p_string |> should equal "p_string" + result.Value.p_ansi_string |> should equal "p_ansi_string" + result.Value.p_boolean |> should equal false + result.Value.p_byte |> should equal Byte.MinValue + result.Value.p_char |> should equal 'a' + result.Value.p_ansi_char |> should equal Char.MinValue + result.Value.p_decimal |> should equal 0.0M + result.Value.p_double |> should equal 0.0 + result.Value.p_float |> should equal 0.0 + result.Value.p_guid |> should equal guidParam + result.Value.p_int16 |> should equal 16s + result.Value.p_int32 |> should equal 32 + result.Value.p_int64 |> should equal 64L + result.Value.p_date_time |> should equal dateTimeParam + [] + member _.``SELECT all sql types using raw input`` () = let guidParam = Guid.NewGuid () let dateTimeParam = DateTime.Now - let param = - [ - "p_null", SqlType.Null - "p_string", sqlString "p_string" - "p_ansi_string", SqlType.AnsiString "p_ansi_string" - "p_boolean", sqlBoolean false - "p_byte", sqlByte Byte.MinValue - "p_char", sqlChar 'a' - "p_ansi_char", SqlType.AnsiChar Char.MinValue - "p_decimal", sqlDecimal 0.0M - "p_double", sqlDouble 0.0 - "p_float", sqlFloat 0.0 - "p_guid", sqlGuid guidParam - "p_int16", sqlInt16 16s - "p_int32", sqlInt32 32 - "p_int64", sqlInt64 64L - "p_date_time", sqlDateTime dateTimeParam - ] conn - |> Db.newCommand sql - |> Db.setParams param + |> Db.newCommand " + SELECT @p_null AS p_null + , @p_string AS p_string + , @p_ansi_string AS p_ansi_string + , @p_boolean AS p_boolean + , @p_byte AS p_byte + , @p_char AS p_char + , @p_ansi_char AS p_ansi_char + , @p_decimal AS p_decimal + , @p_double AS p_double + , @p_float AS p_float + , @p_guid AS p_guid + , @p_int16 AS p_int16 + , @p_int32 AS p_int32 + , @p_int64 AS p_int64 + , @p_date_time AS p_date_time" + |> Db.setParamsRaw [ + "p_null", null + "p_string", "p_string" + "p_ansi_string", "p_ansi_string" + "p_boolean", false + "p_byte", Byte.MinValue + "p_char", 'a' + "p_ansi_char",Char.MinValue + "p_decimal", 0.0M + "p_double", 0.0 + "p_float", 0.0 + "p_guid", guidParam + "p_int16", 16s + "p_int32", 32 + "p_int64", 64L + "p_date_time", dateTimeParam + ] |> Db.querySingle (fun rd -> {| p_null = rd.ReadString "p_null" @@ -373,6 +433,28 @@ type ExecutionTests() = |> Db.query Author.FromReader |> fun result -> result.Length |> should equal 2 + [] + member _.``INSERT MANY RAW authors then count to verify`` () = + let sql = "INSERT INTO author (full_name) VALUES (@full_name);" + + conn + |> Db.newCommand sql + |> Db.execManyRaw + [ [ "full_name", "Tommy Mouse" ] + [ "full_name", "Jerry Cat" ] ] + |> ignore + + let sql = " + SELECT author_id + , full_name + FROM author + WHERE full_name IN ('Tommy Mouse', 'Jerry Cat')" + + conn + |> Db.newCommand sql + |> Db.query Author.FromReader + |> fun result -> result.Length |> should equal 2 + [] member _.``INSERT TRAN MANY authors then count to verify async`` () = use tran = conn.TryBeginTransaction() @@ -406,6 +488,50 @@ type ExecutionTests() = |> Async.RunSynchronously |> fun result -> result.Length |> should equal 2 + [] + member _.``BATCH INSERT authors then count to verify async`` () = + conn + |> Db.batch (fun tran -> + for fullName in [ "Aquaman"; "Flash" ] do + tran + |> Db.newCommandForTransaction "INSERT INTO author (full_name) VALUES (@full_name)" + |> Db.setParams ["full_name", SqlType.String fullName ] + |> Db.exec) + + let sql = " + SELECT author_id + , full_name + FROM author + WHERE full_name IN ('Aquaman', 'Flash')" + + conn + |> Db.newCommand sql + |> Db.query Author.FromReader + |> fun result -> result.Length |> should equal 2 + + [] + member _.``Async BATCH INSERT authors then count to verify async`` () = + conn + |> Db.Async.batch (fun tran -> + tran + |> Db.newCommandForTransaction "INSERT INTO author (full_name) VALUES (@full_name)" + |> Db.Async.execManyRaw [ + ["full_name", "Wonder Woman" ] + ["full_name", "Hawk Girl" ] ] ) + |> Async.AwaitTask + |> Async.RunSynchronously + + let sql = " + SELECT author_id + , full_name + FROM author + WHERE full_name IN ('Wonder Woman', 'Hawk Girl')" + + conn + |> Db.newCommand sql + |> Db.query Author.FromReader + |> fun result -> result.Length |> should equal 2 + [] member _.``INSERT MANY should fail`` () = let sql = "