Skip to content

Commit

Permalink
feat(consistency): meow
Browse files Browse the repository at this point in the history
* feat(consistency): meow
  • Loading branch information
natalie-o-perret authored and Kerry Perret committed Jan 9, 2021
1 parent 488989b commit d550706
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 35 deletions.
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
25 changes: 13 additions & 12 deletions Vp.FSharp.Sql/Helpers.fs
Original file line number Diff line number Diff line change
@@ -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) =
Expand All @@ -26,7 +27,7 @@ type internal DbDataReader with


[<RequireQualifiedAccess>]
module internal String =
module String =

[<Literal>]
let ConnectionStringSeparator = ";"
Expand All @@ -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>

[<AbstractClass; Sealed>]
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


[<RequireQualifiedAccess>]
module internal Async =
module Async =
let linkedTokenSourceFrom cancellationToken =
async {
let! token = Async.CancellationToken
Expand All @@ -62,7 +63,7 @@ module internal Async =


[<RequireQualifiedAccess>]
module internal SkipFirstAsyncSeq =
module SkipFirstAsyncSeq =

let scan folder state source =
AsyncSeq.scan folder state source
Expand All @@ -73,7 +74,7 @@ module internal SkipFirstAsyncSeq =
|> AsyncSeq.skip(1)

[<RequireQualifiedAccess>]
module internal AsyncSeq =
module AsyncSeq =

let mapbi mapping source =
source
Expand All @@ -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<DBNull>

let retypedAs<'T>() = DBNull.Value :> obj :?> 'T
15 changes: 9 additions & 6 deletions Vp.FSharp.Sql/SqlCommand.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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)
Expand Down
11 changes: 11 additions & 0 deletions Vp.FSharp.Sql/SqlDbValue.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[<RequireQualifiedAccess>]
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
85 changes: 68 additions & 17 deletions Vp.FSharp.Sql/Types.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
[<RequireQualifiedAccess>]
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.
[<AbstractClass; Sealed>]
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
Expand Down
1 change: 1 addition & 0 deletions Vp.FSharp.Sql/Vp.FSharp.Sql.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<Compile Include="TransactionScope.fs" />
<Compile Include="Transaction.fs" />
<Compile Include="Types.fs" />
<Compile Include="SqlDbValue.fs" />
<Compile Include="SqlCommand.fs" />
</ItemGroup>

Expand Down

0 comments on commit d550706

Please sign in to comment.