From d550706570832bdacbb5510c72ad7b68e577fc20 Mon Sep 17 00:00:00 2001 From: Kerry Perret <11332444+kerry-perret@users.noreply.github.com> Date: Sat, 9 Jan 2021 21:15:17 +0100 Subject: [PATCH] feat(consistency): meow * feat(consistency): meow --- README.md | 26 +++++++++ Vp.FSharp.Sql/Helpers.fs | 25 ++++----- Vp.FSharp.Sql/SqlCommand.fs | 15 +++--- Vp.FSharp.Sql/SqlDbValue.fs | 11 ++++ Vp.FSharp.Sql/Types.fs | 85 ++++++++++++++++++++++++------ Vp.FSharp.Sql/Vp.FSharp.Sql.fsproj | 1 + 6 files changed, 128 insertions(+), 35 deletions(-) create mode 100644 Vp.FSharp.Sql/SqlDbValue.fs diff --git a/README.md b/README.md index 1782b1f..7d1f9c2 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,32 @@ TBD | [![Semantic Release](https://img.shields.io/badge/Semantic%20Release-17 ---------------- | -------- | ------- | `Vp.FSharp.Sql` | [![NuGet Status](http://img.shields.io/nuget/v/Vp.FSharp.Sql.svg)](https://www.nuget.org/packages/Vp.FSharp.Sql) | `Install-Package Vp.FSharp.Sql` +# Why did you folks create this lib? + +## Motivations + +- We all agree that Dapper is a great library +- We also wanted something even more bare-bone and more functional / idiomatic-F# +- Inspired by [Zaid's Npgsql.FSharp](https://github.com/Zaid-Ajaj/Npgsql.FSharp) but with + - Multi DB-providers support + - Explicit operations flow + - Transaction Helpers + - Dapper connection workflow + - (Very) limited support for basic events (ie. logging) + - Opinionated (ie. async only, not result as return types) + +# How to use this library? + +This library mostly aims at being used some sort of building block with ADO.NET providers to provide a strongly-typed, +you can check out the libraries below, each leveraging a specific ADO.NET provider: +- [Vp.FSharp.Sql.PostgreSql](https://github.com/veepee-oss/Vp.FSharp.Sql.PostgreSql) +- [Vp.FSharp.Sql.SqlServer](https://github.com/veepee-oss/Vp.FSharp.Sql.SqlServer) +- [Vp.FSharp.Sql.Sqlite](https://github.com/veepee-oss/Vp.FSharp.Sql.Sqlite) + +In a Nutshell you can create your own provider by: +- Using the relevant generic providers `SqlDeps` (this is used due to the lack of member support for the SRTP) +- + # How to Contribute Bug reports, feature requests, and pull requests are very welcome! Please read the [Contribution Guidelines](./CONTRIBUTION.md) to get started. diff --git a/Vp.FSharp.Sql/Helpers.fs b/Vp.FSharp.Sql/Helpers.fs index d4f6ebd..a6ad662 100644 --- a/Vp.FSharp.Sql/Helpers.fs +++ b/Vp.FSharp.Sql/Helpers.fs @@ -1,20 +1,21 @@ module internal Vp.FSharp.Sql.Helpers open System -open System.Data.Common open System.Threading -open System.Text.RegularExpressions -open System.Threading.Tasks +open System.Data.Common open System.Transactions +open System.Threading.Tasks +open System.Text.RegularExpressions open FSharp.Control -type internal DbConnection with +type DbConnection with member this.EnlistCurrentTransaction() = this.EnlistTransaction(Transaction.Current) -type internal DbDataReader with +type DbDataReader with + member this.AwaitRead(cancellationToken) = this.ReadAsync(cancellationToken) |> Async.AwaitTask member this.AwaitNextResult(cancellationToken) = this.NextResultAsync(cancellationToken) |> Async.AwaitTask member this.AwaitTryReadNextResult(cancellationToken) = @@ -26,7 +27,7 @@ type internal DbDataReader with [] -module internal String = +module String = [] let ConnectionStringSeparator = ";" @@ -43,16 +44,16 @@ module internal String = let stitch strs = String.concat SqlNewLineConstant strs -let internal def<'T> = Unchecked.defaultof<'T> +let def<'T> = Unchecked.defaultof<'T> [] -type internal Async private () = +type Async private () = static member AwaitValueTask(valueTask: ValueTask) = valueTask.AsTask() |> Async.AwaitTask static member AwaitValueTask(valueTask: ValueTask<'T>) = valueTask.AsTask() |> Async.AwaitTask [] -module internal Async = +module Async = let linkedTokenSourceFrom cancellationToken = async { let! token = Async.CancellationToken @@ -62,7 +63,7 @@ module internal Async = [] -module internal SkipFirstAsyncSeq = +module SkipFirstAsyncSeq = let scan folder state source = AsyncSeq.scan folder state source @@ -73,7 +74,7 @@ module internal SkipFirstAsyncSeq = |> AsyncSeq.skip(1) [] -module internal AsyncSeq = +module AsyncSeq = let mapbi mapping source = source @@ -97,7 +98,7 @@ module internal AsyncSeq = let consume source = AsyncSeq.iter(fun _ -> ()) source -module internal DbNull = +module DbNull = let is<'T>() = typedefof<'T> = typedefof let retypedAs<'T>() = DBNull.Value :> obj :?> 'T diff --git a/Vp.FSharp.Sql/SqlCommand.fs b/Vp.FSharp.Sql/SqlCommand.fs index 21e5b60..0b43c71 100644 --- a/Vp.FSharp.Sql/SqlCommand.fs +++ b/Vp.FSharp.Sql/SqlCommand.fs @@ -29,21 +29,24 @@ module Vp.FSharp.Sql.SqlCommand CommandType = DefaultCommandType Prepare = DefaultPrepare Transaction = None - Logger = Conf } + Logger = LoggerKind.Configuration } /// Initialize a command definition with the given text contained in the given string. let text value = { defaultCommandDefinition() with Text = Text.Single value } /// Initialize a command definition with the given text spanning over several strings (ie. list). - let textFromList value = { defaultCommandDefinition() with Text = Text.Multiple value } + let textFromList value = + { defaultCommandDefinition() with Text = Text.Multiple value } /// Update the command definition so that when executing the command, it doesn't use any logger. /// Be it the default one (Global, if any.) or a previously overriden one. - let noLogger commandDefinition = { commandDefinition with Logger = LoggerKind.Nothing } + let noLogger commandDefinition = + { commandDefinition with Logger = Nothing } /// Update the command definition so that when executing the command, it use the given overriding logger. /// instead of the default one, aka the Global logger, if any. - let overrideLogger value commandDefinition = { commandDefinition with Logger = LoggerKind.Override value } + let overrideLogger value commandDefinition = + { commandDefinition with Logger = LoggerKind.Override value } /// Update the command definition with the given parameters. let parameters value commandDefinition = { commandDefinition with Parameters = value } @@ -97,9 +100,9 @@ module Vp.FSharp.Sql.SqlCommand connection.OpenAsync(cancellationToken) |> Async.AwaitTask - let private log4 conf commandDefinition sqlLog = + let private log4 configuration commandDefinition sqlLog = match commandDefinition.Logger with - | Conf -> conf.DefaultLogger + | Configuration -> configuration.DefaultLogger | Override logging -> Some logging | Nothing -> None |> Option.iter (fun f -> f sqlLog) diff --git a/Vp.FSharp.Sql/SqlDbValue.fs b/Vp.FSharp.Sql/SqlDbValue.fs new file mode 100644 index 0000000..8125724 --- /dev/null +++ b/Vp.FSharp.Sql/SqlDbValue.fs @@ -0,0 +1,11 @@ +[] +module Vp.FSharp.Sql.NullDbValue + + +let ifNone someDbValue noneDbValue = function + | Some value -> someDbValue value + | None -> noneDbValue + +let ifError okDbValue errorDbValue = function + | Ok value -> okDbValue value + | Error error -> errorDbValue error diff --git a/Vp.FSharp.Sql/Types.fs b/Vp.FSharp.Sql/Types.fs index 5c9d049..a7b488a 100644 --- a/Vp.FSharp.Sql/Types.fs +++ b/Vp.FSharp.Sql/Types.fs @@ -9,79 +9,130 @@ open System.Threading.Tasks open Vp.FSharp.Sql.Helpers +/// The type that represents the text of the command that is going to be run against the connection data source. type Text = + /// The text is represented as a single string. | Single of string + /// The text is represented as multiple strings. | Multiple of string list + +/// The type representing the different sql logs available. type SqlLog<'DbConnection, 'DbCommand when 'DbConnection :> DbConnection and 'DbCommand :> DbCommand> = + /// The connection has just been opened. | ConnectionOpened of connection: 'DbConnection + /// The connection has just been closed. | ConnectionClosed of connection: 'DbConnection * sinceOpened: TimeSpan + /// The command is just done being prepared and ready to be executed. | CommandPrepared of command: 'DbCommand + /// The command is just done being executed. | CommandExecuted of command: 'DbCommand * sincePrepared: TimeSpan +/// The type representing the different kinds of logger available. type LoggerKind<'DbConnection, 'DbCommand when 'DbConnection :> DbConnection and 'DbCommand :> DbCommand> = - | Conf + /// Default value: the one defined in the configuration, if any. + | Configuration + /// The default one is overriden and instead use this given value. | Override of (SqlLog<'DbConnection, 'DbCommand> -> unit) + /// Nothing, ie. no logger assigned upon command execution. | Nothing -type CommandDefinition<'DbConnection, 'DbTransaction, 'DbCommand, 'DbParameter, 'DbDataReader, 'DbType +/// Contains the definition of a command upon its execution +type CommandDefinition<'DbConnection, 'DbCommand, 'DbParameter, 'DbDataReader, 'DbTransaction, 'DbType when 'DbConnection :> DbConnection - and 'DbTransaction :> DbTransaction and 'DbCommand :> DbCommand and 'DbParameter :> DbParameter - and 'DbDataReader :> DbDataReader> = - { Text: Text + and 'DbDataReader :> DbDataReader + and 'DbTransaction :> DbTransaction> = + { /// The text of the command that is going to be run against the connection data source. + Text: Text + + /// The parameters of the SQL statement or stored procedure. Parameters: (string * 'DbType) list + + /// A cancellation token that can be used to request the operation to be cancelled early. CancellationToken: CancellationToken + + /// The wait time before terminating the attempt to execute a command and generating an error. Timeout: TimeSpan + + /// The way how the text is interpreted. CommandType: CommandType + + /// Indicates whether a prepared (or compiled) version of the command on the data source has to be done Prepare: bool + + /// The transactions within which the command is going to be executed. Transaction: 'DbTransaction option + + /// The logger to call upon events occurence. Logger: LoggerKind<'DbConnection, 'DbCommand> } -type SqlConf<'DbConnection, 'DbCommand +/// A data structure holding some configuration with the relevant generic constraints. +type SqlConfiguration<'DbConnection, 'DbCommand when 'DbConnection :> DbConnection and 'DbCommand :> DbCommand> = { DefaultLogger: (SqlLog<'DbConnection, 'DbCommand> -> unit) option } +/// The related module handling operations on configuration. [] -module SqlConf = +module SqlConfiguration = let internal defaultValue() = { DefaultLogger = None } - let logger value conf = { conf with DefaultLogger = Some value } - let noLogger conf = { conf with DefaultLogger = None } + /// Setting up the configuration + let logger value (configuration: SqlConfiguration<'DbConnection, 'DbCommand>) = + { configuration with DefaultLogger = Some value } + + /// Defines no logger for the given configuration. + let noLogger (configuration: SqlConfiguration<'DbConnection, 'DbCommand>) = + { configuration with DefaultLogger = None } +/// A configuration cache holding a single value per set of generic constraints +/// and giving an access to a snapshot at any given point in time. +/// Can serve and act as some sort of global configuration. [] -type SqlGlobalConf<'DbConnection, 'DbCommand +type SqlConfigurationCache<'DbConnection, 'DbCommand when 'DbConnection :> DbConnection and 'DbCommand :> DbCommand> private() = - static let mutable instance: SqlConf<'DbConnection, 'DbCommand> = SqlConf.defaultValue() + static let mutable instance: SqlConfiguration<'DbConnection, 'DbCommand> = SqlConfiguration.defaultValue() static member Snapshot with get () = instance - static member Logger(value) = instance <- SqlConf.logger value instance - static member NoLogger() = instance <- SqlConf.noLogger instance + static member Logger(value) = instance <- SqlConfiguration.logger value instance + static member NoLogger() = instance <- SqlConfiguration.noLogger instance + -type SqlDeps<'DbConnection, 'DbTransaction, 'DbCommand, 'DbParameter, 'DbDataReader, 'DbType +// Ie. The ADO.NET Provider generic constraints mapper due to the lack of proper support for some variant of the SRTP +// and the hideous members shadowing occuring in most ADO.NET Providers implementation +type SqlDependencies<'DbConnection, 'DbCommand, 'DbParameter, 'DbDataReader, 'DbTransaction, 'DbType when 'DbConnection :> DbConnection - and 'DbTransaction :> DbTransaction and 'DbCommand :> DbCommand and 'DbParameter :> DbParameter - and 'DbDataReader :> DbDataReader> = + and 'DbDataReader :> DbDataReader + and 'DbTransaction :> DbTransaction> = { CreateCommand: 'DbConnection -> 'DbCommand ExecuteReaderAsync: 'DbCommand -> CancellationToken -> Task<'DbDataReader> DbValueToParameter: string -> 'DbType -> 'DbParameter } +// Represents a field collected by the SqlRecordReader type DbField = - { Name: string + { /// The field name as found in the result set. + Name: string + + /// The field name as found in the result set. Index: int32 + + /// The assigned .NET type assigned to this field. NetTypeName: string + + /// The field native type name as found in the result set. NativeTypeName: string } +// Wrap a specific DataReader type SqlRecordReader<'DbDataReader when 'DbDataReader :> DbDataReader>(dataReader: 'DbDataReader) = let mapFieldIndex fieldIndex = { Index = fieldIndex diff --git a/Vp.FSharp.Sql/Vp.FSharp.Sql.fsproj b/Vp.FSharp.Sql/Vp.FSharp.Sql.fsproj index a2dc4cc..518691a 100644 --- a/Vp.FSharp.Sql/Vp.FSharp.Sql.fsproj +++ b/Vp.FSharp.Sql/Vp.FSharp.Sql.fsproj @@ -22,6 +22,7 @@ +