Coder Social home page Coder Social logo

dzoukr / dapper.fsharp Goto Github PK

View Code? Open in Web Editor NEW
359.0 359.0 35.0 372 KB

Lightweight F# extension for StackOverflow Dapper with support for MSSQL, MySQL, PostgreSQL, and SQLite

License: MIT License

F# 100.00%
dapper database fsharp linq mssql mysql postgresql sqlite

dapper.fsharp's Introduction

Dapper.FSharp NuGet

Lightweight F# extension for StackOverflow Dapper with support for MSSQL, MySQL, PostgreSQL and SQLite

Features

  • No auto-attribute-based-only-author-maybe-knows-magic behavior
  • Support for (anonymous) F# records
  • Support for F# options
  • LINQ Query Provider
  • Support for SQL Server 2012 (11.x) and later / Azure SQL Database, MySQL 8.0, PostgreSQL 12.0, SQLite 3
  • Support for SELECT (including JOINs), INSERT, UPDATE (full / partial), DELETE
  • Support for OUTPUT clause (MSSQL only)
  • Support for INSERT OR REPLACE clause (SQLite)
  • Easy usage thanks to F# computation expressions
  • Keeps things simple

Installation

If you want to install this package manually, use usual NuGet package command

Install-Package Dapper.FSharp

or using Paket

paket add Dapper.FSharp

What's new in v4?

Reasoning behind version 4 is described in this issue, but the main changes are:

  • Each database provider has its own query definition
  • New database-specific keywords for MSSQL & Postgres
  • Operators considered harmful (removed for functions IN, NOT IN, LIKE and NOT LIKE)
  • Minimal supported version is NET 6.0

If you still need/want to use v3.0, follow the Version 3 docs.

FAQ

Why another library around Dapper?

I've created this library to cover most of my own use-cases where in 90% I need just a few simple queries for CRUD operations using Dapper and don't want to write column names manually. All I need is a simple record with properties and want to have them filled from the query or to insert / update data.

How does the library works?

This library does two things:

  1. Provides 4 computation expression builders for select, insert, update and delete. Those expressions create definitions (just simple records, no worries) of SQL queries.
  2. Extends IDbConnection with few more methods to handle such definitions and creates proper SQL query + parameters for Dapper. Then it calls Dapper QueryAsync or ExecuteAsync. How does the library know the column names? It uses reflection to get record properties. So yes, there is one (the only) simple rule: All property names must match columns in the table.

Do I need to create a record with all columns?

You can, but don't have to. If you need to read a subset of data only, you can create a special view record just for this. Also if you don't want to write nullable data, you can omit them in the record definition.

And what about names mapping using Attributes or foreign keys magic?

Nope. Sorry. Not gonna happen in this library. Simplicity is what matters. Just define your record as it is in a database and you are ok.

Can I map more records from one query?

Yes. If you use LEFT or INNER JOIN, you can map each table to a separate record. If you use LEFT JOIN, you can even map the 2nd and/or 3rd table to Option (F# records and null values don't work well together). The current limitation is 3 tables (two joins).

What if I need to join more than 3 tables, sub-select or something special?

Fallback to plain Dapper then. Really. Dapper is an amazing library and sometimes there's nothing better than manually written optimized SQL query. Remember this library has one and only goal: Simplify 90% of repetitive SQL queries you would have to write manually. Nothing. Else.

Getting started

First of all, you need to init registration of mappers for optional types to have Dapper mappings understand that NULL from database = Option.None

// for MSSQL
Dapper.FSharp.MSSQL.OptionTypes.register()

// for MySQL
Dapper.FSharp.MySQL.OptionTypes.register()

// for PostgreSQL
Dapper.FSharp.PostgreSQL.OptionTypes.register()

// for SQLite
Dapper.FSharp.SQLite.OptionTypes.register()

It's recommended to do it somewhere close to the program entry point or in Startup class.

Example database

Let's have a database table called Persons:

CREATE TABLE [dbo].[Persons](
    [Id] [uniqueidentifier] NOT NULL,
    [FirstName] [nvarchar](max) NOT NULL,
    [LastName] [nvarchar](max) NOT NULL,
    [Position] [int] NOT NULL,
    [DateOfBirth] [datetime] NULL)

As mentioned in FAQ section, you need F# record to work with such table in Dapper.FSharp:

type Person = {
    Id : Guid
    FirstName : string
    LastName : string
    Position : int
    DateOfBirth : DateTime option
}

If you prefer not exposing your records, you can use internal types:

type internal Person = {
    Id : Guid
    FirstName : string
    LastName : string
    Position : int
    DateOfBirth : DateTime option
}

Hint: Check tests located under tests/Dapper.FSharp.Tests folder for more examples

API Overview

Table Mappings

You can either specify your tables within the query, or you can specify them above your queries (which is recommended since it makes them sharable between your queries). The following will assume that the table name exactly matches the record name, "Person":

let personTable = table<Person>

If your record maps to a table with a different name:

let personTable = table'<Person> "People"

If you want to include a schema name:

let personTable = table'<Person> "People" |> inSchema "dbo"

INSERT

Inserting a single record:

open Dapper.FSharp.MSSQL

let conn : IDbConnection = ... // get it somewhere

let newPerson = { Id = Guid.NewGuid(); FirstName = "Roman"; LastName = "Provaznik"; Position = 1; DateOfBirth = None }

let personTable = table<Person>

insert {
    into personTable
    value newPerson
} |> conn.InsertAsync

Inserting Multiple Records:

open Dapper.FSharp.MSSQL

let conn : IDbConnection = ... // get it somewhere

let person1 = { Id = Guid.NewGuid(); FirstName = "Roman"; LastName = "Provaznik"; Position = 1; DateOfBirth = None }
let person2 = { Id = Guid.NewGuid(); FirstName = "Ptero"; LastName = "Dactyl"; Position = 2; DateOfBirth = None }

let personTable = table<Person>

insert {
    into personTable
    values [ person1; person2 ]
} |> conn.InsertAsync

Excluding Fields from the Insert:

open Dapper.FSharp.MSSQL

let conn : IDbConnection = ... // get it somewhere

let newPerson = { Id = Guid.NewGuid(); FirstName = "Roman"; LastName = "Provaznik"; Position = 1; DateOfBirth = None }

let personTable = table<Person>

insert {
    for p in personTable do
    value newPerson
    excludeColumn p.DateOfBirth
} |> conn.InsertAsync

NOTE: You can exclude multiple fields by using multiple excludeColumn statements.

UPDATE

let updatedPerson = { existingPerson with LastName = "Vorezprut" }

update {
    for p in personTable do
    set updatedPerson
    where (p.Id = updatedPerson.Id)
} |> conn.UpdateAsync

Partial updates are possible by manually specifying one or more includeColumn properties:

update {
    for p in personTable do
    set modifiedPerson
    includeColumn p.FirstName
    includeColumn p.LastName
    where (p.Position = 1)
} |> conn.UpdateAsync

Partial updates are also possible by using setColumn keyword:

update {
    for p in personTable do
    setColumn p.FirstName "UPDATED"
    setColumn p.LastName "UPDATED"
    where (p.Position = 1)
} |> conn.UpdateAsync

DELETE

delete {
    for p in personTable do
    where (p.Position = 10)
} |> conn.DeleteAsync

And if you really want to delete the whole table, you must use the deleteAll keyword:

delete {
    for p in personTable do
    deleteAll
} |> conn.DeleteAsync

SELECT

To select all records in a table, you must use the selectAll keyword:

select {
    for p in personTable do
    selectAll
} |> conn.SelectAsync<Person>

NOTE: You also need to use selectAll if you have a no where and no orderBy clauses because a query cannot consist of only for or join statements.

Filtering with where statement:

select {
    for p in personTable do
    where (p.Position > 5 && p.Position < 10)
} |> conn.SelectAsync<Person>

To flip boolean logic in where condition, use not operator (unary NOT):

select {
    for p in personTable do
    where (not (p.Position > 5 && p.Position < 10))
} |> conn.SelectAsync<Person>

You can also combine multiple where conditions with andWhere and orWhere:

select {
    for p in personTable do
    where (p.Position > 5)
    andWhere (p.Position < 10)
    orWhere (p.Position < 2)
} |> conn.SelectAsync<Person>

To conditionally add where part, you can use andWhereIf and orWhereIf:

let pos = Some 10
let posOr = Some 2
select {
    for p in personTable do
    where (p.Position > 5)
    andWhereIf pos.IsSome (p.Position < pos.Value)
    orWhereIf posOr.IsSome (p.Position < posOr.Value)
} |> conn.SelectAsync<Person>

NOTE: Do not use the forward pipe |> operator in your query expressions because it's not implemented, so don't do it (unless you like exceptions)!

To use LIKE operator in where condition, use like:

select {
    for p in personTable do
    where (like p.FirstName "%partofname%")
} |> conn.SelectAsync<Person>

To use IN operator in where condition, use isIn:

select {
    for p in personTable do
    where (isIn p.FirstName ["Elizabeth"; "Philipp"])
} |> conn.SelectAsync<Person>

You can also negate the IN operator in where condition, with isNotIn:

select {
    for p in personTable do
    where (isNotIn p.FirstName ["Charles"; "Camilla"])
} |> conn.SelectAsync<Person>

Sorting:

select {
    for p in personTable do
    where (p.Position > 5 && p.Position < 10)
    orderBy p.Position
    thenByDescending p.LastName
} |> conn.SelectAsync<Person>

If you need to skip some values or to take a subset of results only, use skip, take, and skipTake. Keep in mind that for correct paging, you need to order results as well.

select {
    for p in personTable do
    where (p.Position > 5 && p.Position < 10)
    orderBy p.Position
    skipTake 2 3 // skip first 2 rows, take next 3
} |> conn.SelectAsync<Person>

Option Types and Nulls

Checking for null on an Option type:

select {
    for p in personTable do
    where (p.DateOfBirth = None)
    orderBy p.Position
} |> conn.SelectAsync<Person>

Checking for null on a nullable type:

select {
    for p in personTable do
    where (p.LastName = null)
    orderBy p.Position
} |> conn.SelectAsync<Person>

Checking for null (works for any type):

select {
    for p in personTable do
    where (isNullValue p.LastName && isNotNullValue p.FirstName)
    orderBy p.Position
} |> conn.SelectAsync<Person>

Comparing an Option Type

let dob = DateTime.Today

select {
    for p in personTable do
    where (p.DateOfBirth = Some dob)
    orderBy p.Position
} |> conn.SelectAsync<Person>

JOINS

For simple queries with join, you can use innerJoin and leftJoin in combination with SelectAsync overload:

let personTable = table<Person>
let dogsTable = table<Dog>
let dogsWeightsTable = table<DogsWeight>

select {
    for p in personTable do
    innerJoin d in dogsTable on (p.Id = d.OwnerId)
    orderBy p.Position
} |> conn.SelectAsync<Person, Dog>

Dapper.FSharp will map each joined table into a separate record and return it as list of 'a * 'b tuples. Currently, up to 4 joins are supported, so you can also join another table here:

select {
    for p in personTable do
    innerJoin d in dogsTable on (p.Id = d.OwnerId)
    innerJoin dw in dogsWeightsTable on (d.Nickname = dw.DogNickname)
    orderBy p.Position
} |> conn.SelectAsync<Person, Dog, DogsWeight>

The problem with LEFT JOIN is that tables "on the right side" can be full of null values. Luckily we can use SelectAsyncOption to map joined values to Option types:

// this will return seq<(Person * Dog option * DogWeight option)>
select {
    for p in personTable do
    leftJoin d in dogsTable on (p.Id = d.OwnerId)
    leftJoin dw in dogsWeightsTable on (d.Nickname = dw.DogNickname)
    orderBy p.Position
} |> conn.SelectAsyncOption<Person, Dog, DogsWeight>

Aggregate functions

Aggregate functions include count, avg, sum, min, and max. To fully support these functions in builder syntax, the groupBy, groupByMany and distinct keywords are supported as well.

See this example of how to get the amount of persons having a position value greater than 5:

select {
    for p in persons do
    count "*" "Value" // column name and alias (must match the view record property!!!)
    where (p.Position > 5)
} |> conn.SelectAsync<{| Value : int |}>

Or get the maximum value of the Position column from the table:

select {
    for p in persons do
    max "Position" "Value"
} |> conn.SelectAsync<{| Value : int |}>

Please keep in mind that work with aggregate functions can quickly turn into a nightmare. Use them wisely and if you'll find something hard to achieve using this library, better fallback to plain Dapper and good old handwritten queriesโ„ข.

OUTPUT clause support (MSSQL & PostgreSQL only)

This library supports OUTPUT clause for MSSQL & PostgreSQL using special methods: InsertOutputAsync, UpdateOutputAsync and DeleteOutputAsync. Please check tests located under tests/Dapper.FSharp.Tests folder for more examples.

INSERT or REPLACE (SQLite only)

This library supports INSERT or REPLACE clause for SQLite using special method: InsertOrReplaceAsync.

Deconstructor

To provide better usage with plain Dapper, this library contains Deconstructor converting Dapper.FSharp queries to a tuple of parameterized SQL query and Map of parameter values.

let r = {
    Id = Guid.NewGuid()
    FirstName = "Works"
    LastName = "Great"
    DateOfBirth = DateTime.Today
    Position = 1
}

let sql, values =
    insert {
        into personTable
        value r
    } |> Deconstructor.insert

printfn "%s" sql 
// INSERT INTO Persons (Id, FirstName, LastName, Position, DateOfBirth) 
// VALUES (@Id0, @FirstName0, @LastName0, @Position0, @DateOfBirth0)"

printfn "%A" values
// map [("DateOfBirth0", 11.05.2020 0:00:00); 
//      ("FirstName0", "Works");
//      ("Id0", 8cc6a7ed-7c17-4bea-a0ca-04a3985d2c7e); 
//      ("LastName0", "Great");
//      ("Position0", 1)]

Database-specific syntax

Since version 4 Dapper.FSharp supports database-specific syntax.

MSSQL

Query Keyword Description
SELECT optionRecompile Adds OPTION(RECOMPILE) as the query option
SELECT optionOptimizeForUnknown Adds OPTION(OPTIMIZE FOR UNKNOWN) as the query option

PostgreSQL

Query Keyword Description
ALL iLike Adds ILIKE for WHERE condition
ALL notILike Adds NOT ILIKE for WHERE condition

IncludeColumn vs ExcludeColumn (there can be a ๐Ÿฒ)

New keywords added in v2 - excludeColumn and includeColumn are a great addition to this library, especially when you want to do partial updates / inserts. However, be aware that you should never mix both in the same computation expression!

ExcludeColumn

If used for the first time within computation expression all fields from the record will be used and removed (ignored) those you provided in a keyword. When used more times, already filtered fields will be filtered again.

IncludeColumn

If used, only specified columns will be used and all the others will be ignored.

With great power comes great responsibility.

Contribution Guide

Every new idea of how to make the library even better is more than welcome! However please be aware that there is a process we should all follow:

  • Create an issue with a description of proposed changes
  • Describe the expected impact on the library (API, performance, ...)
  • Define if it's minor or breaking change
  • Wait for Approve / Deny
  • Send a PR (or wait until taken by some of the contributors)

dapper.fsharp's People

Contributors

1eyewonder avatar dawedawe avatar dzoukr avatar jindraivanek avatar jordanmarr avatar kstastny avatar prosconi avatar randrag avatar simonmcconnell avatar terencehinrichsen avatar tforkmann avatar theangrybyrd avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

dapper.fsharp's Issues

CancellationToken support

Hey there!

Problem statement

I started playing with this library a bit more in depth and one of the things missing for me is CancellationToken support. The reason passing down a CancellationToken to is so database connections don't stay open longer than they need for.

For example, a user could create a complex search, start the query, then decide to do something else, and move away from the page. Without passing along the CancellationToken to the database query, it may run in the background for some time, consuming resources.

Changes

It looks like query1 and execute in IDbConnection.fs would need to accept a CancellationToken. Since this is an internal module it should be a safe change.

Example of the change proposed:

let query1<'a> (this:IDbConnection) trans timeout cancellationToken (logFunction:LogFn option) (query, pars) =
    if logFunction.IsSome then (query, pars) |> logFunction.Value
    CommandDefinition(query, pars, ?transaction = trans, ?commandTimeout = timeout, ?cancellationToken = cancellationToken)
    |> this.QueryAsync<'a>

Additionally the providers will need to add optional parameters to the public queries such as SelectAsync

    member this.SelectAsync<'a> (q:SelectQuery, ?trans:IDbTransaction, ?timeout:int, ?logFunction, ?cancellationToken : CancellationToken) =
        q |> Deconstructor.select<'a> |> IDbConnection.query1<'a> this trans timeout cancellationToken logFunction

Since this is an optional parameter, this should not be a breaking change.

I could not find support for query2 because in dapper the underlying calls don't support a CancellationToken so this will remain the same unfortunately.

Question on usage of insert

Hi,

Is this what your insert code ends up looking like? I had to put the () unit in to appease the compiler.

let a = { foo = "bar }
task {
    let! _ =
        insert {
            table "EventLog"
            values a
        } |> sqlConn.InsertAsync
    ()
} |> Async.AwaitTask |> Async.RunSynchronously  // or Async.Start

Does Dapper.FSharp support records when using QueryAsync?

This is mostly a clarification post, rather than requesting any functionality.

On the readme it says this supports "Support for F# records / anonymous records", but I'm not sure if that is limited to the CE builder stuff, or if it includes QueryAsync.

When I use connection.QueryAsync(mySqlText, myParams) I can get anonymous records to work no problem, but an F# record gives me the no parameterless constructor found.

Is this expected, or have I messed something up?

Thanks for the great library, and your time reading this post.

Question: Logging query parameter values

The signature for logging function was before 1.13.0 string * Map<string, obj> -> unit and in 1.13.0 it was changed to string * obj -> unit.

After the change I have had trouble printing the parameters correctly, especially when obj is Map<string, List<string, obj>> (this happens when using isIn function in where clause). I tried to pattern match the runtime type but had no luck there. Following example is an attempt to log isIn parameters.

match values with
| :? Map<string, System.Collections.Generic.List<string>> as map ->
    map
    |> Seq.map (fun (KeyValue(key,value)) -> sprintf "%s: %s" key (String.concat ", " value))
    |> String.concat Environment.NewLine
 | _ ->
    string values // All other cases so far kinda work straight of obj

// Output: map [(Where_Type1, System.Collections.Generic.List`1[System.String])]

How would you implement the query logging function for parameter values?

Write a "Contribution guide" section in README

After great contribution by @JordanMarr (and a tiny fuzz about PR with include / exclude from my side), there should be a section in README with defined contribution procedure like:

  • Create Issue
  • Describe desired functionality
  • Discuss the impact on library
  • Discuss if it's a feature or major change
  • Approve / Deny
  • Create PR

Major goal: Avoid future misunderstandings and do not waste anyone's good intended PR.

Sqlite and multiple type registration

When using sqlite you have to register types using this way

OptionTypes.register ()
Dapper.FSharp.SQLite.TypeHandlers.addSQLiteTypeHandlers ()

but you register some type converter more than once.
For example TimeSpan option
I found that dapper sometime select convertor from OptionTypes.register() and sometime from addSQLiteTypeHandlers().
For example for TimeSpan it is problem, because convertor from OptionTypes.register() don't use TimeSpan.Parse and throws exception.

Make README code samples valid (Add requirement for `[<CLIMutable>]` attribute to docs)

It seems based on guides and posts elsewhere that F# record types must be annotated with [<CLIMutable>] to interoperate with Dapper queries. In my own experience, following the code samples in the README leads to exceptions that are not readily resolvable without some significant further digging.

It would be helpful for the samples in the README to be valid and working code.

Arrays are not covered in the OptionType-registration

Hi!

We are evaluating Dapper.FSharp. First thing we tried is to replace our registration-bag for OptionsTypes. You may have seen something like this before :-)

    let register() =
        SqlMapper.AddTypeHandler (OptionHandler<Guid>())
        SqlMapper.AddTypeHandler (OptionHandler<byte>())
        SqlMapper.AddTypeHandler (OptionHandler<byte[]>())
        SqlMapper.AddTypeHandler (OptionHandler<int16>())
        SqlMapper.AddTypeHandler (OptionHandler<int>())
        SqlMapper.AddTypeHandler (OptionHandler<int64>())
        SqlMapper.AddTypeHandler (OptionHandler<float>())
        SqlMapper.AddTypeHandler (OptionHandler<decimal>())
        SqlMapper.AddTypeHandler (OptionHandler<float32>())
        SqlMapper.AddTypeHandler (OptionHandler<string>())
        SqlMapper.AddTypeHandler (OptionHandler<char>())
        SqlMapper.AddTypeHandler (OptionHandler<DateTime>())
        SqlMapper.AddTypeHandler (OptionHandler<DateTimeOffset>())
        SqlMapper.AddTypeHandler (OptionHandler<bool>())
        SqlMapper.AddTypeHandler (OptionHandler<TimeSpan>())

We found that it works for most type, but not for the byte[] array type. So we cut it down to

    let register() =
          Dapper.FSharp.OptionTypes.register()
          SqlMapper.AddTypeHandler (OptionHandler<byte[]>())

Do you think, Dapper.FSharp could cover those as well?

Best regards,
Wolfram

SelectAsync returns a Task which cannot be wrapped in async block

This does not work:

namespace API.DAL.Authorization

open API.DAL
open Dapper.FSharp
open Dapper.FSharp.MSSQL
open Microsoft.Data.SqlClient
open System.Data

type public IAuthorizationRepository =
    abstract member ListModules : slug:string -> Async<string list>

type public AuthorizationRepository(connectionString: ConnectionString) =
    interface IAuthorizationRepository with
        member this.ListModules(slug: string) = 
            use connection: IDbConnection = new SqlConnection(connectionString.ConnectionString)
            let moduleTable = table<string>
            async {
                select {
                    for p in moduleTable do
                    selectAll
                } |> connection.SelectAsync<string>
                // ??? Compile error: Type constraint mismatch. The type Async<unit> is not compatible with type Async<string list>
            }

How to use Dapper.FSharp with async?

Update table with dynamic column name?

Not exactly a big deal but wanting to do a simple update column function

eg let updateColumn key column value

Looking at the partial update expression, Iโ€™d want something similar to this:

let updateColumn key column value  =
    update {
        table "Persons"
        set {| column = value |}
        where (eq "person-id" key)
    } |> conn.UpdateAsync

Is this possible? Computation expressions are very confusing to me!

exception raised when column names are reserved names

type MyDatabaseProjection = {
    Id : int
    Name : string
    Desc : string
}

let getById connectionString id = task {
    use c = new SqlConnection(connectionString)
    return! select { table "MyTable" where (eq "Id" id) } |> c.SelectAsync<MyDatabaseProjection>
}

This will give SELECT Id, Desc FROM MyTable WHERE Id=1 and raise an exception, when it really just needs to be something like SELECT [Id], [Desc] FROM [MyTable] WHERE Id=1. Do you have any objections to putting square brackets around the terms?

Is there a way to do upserts in PostgreSQL

I'm relying on using INSERT ... ON CONFLICT (internal_id) DO NOTHING or ``INSERT ... ON CONFLICT (internal_id) DO UPDATE ...` operations. Especially those updates are tedious to write for wider tables (each property name repeated like 5 times...). Is there a way this library could help me?

If the answer is no, but you'd like to have this functionality, I'd be happy to implement that.

Add overload on innerJoin keyword to allow join on multiple columns

Allows the user to specify either the current setup tableName, colName, col2Name OR
tableName, joinList
Example:

select {
      table "Dogs"
      innerJoin "VaccinationHistory" ["PetOwnerId", "Dogs.OwnerId"; "DogNickname", "Dogs.Nickname"]
      orderBy "Dogs.Nickname" Asc
            } 

We have been using this extensively and it works extremely well in our "eventsourced" tables where we have the EntityId and Version as a composite primary key.

Requires update to Net 5.0

I am able to test on MSSQL, but I am having a difficult time with getting the MySQL and PostGres tested, would someone be able to help with those?

Semantic inconsistency of `+` / `*` for And / Or operators

There's this weird operator assignment of (+) -> and and (*) -> or for Select expressions.

The problem is that this is the exact opposite of what one would normally semantically expect these operators to mean...

For example in F# we have Sum and Product types where Sum type (discriminate union) means an OR (one-of given options)
and Product type (record) means an AND (all attributes in a type are present).

Similarly it is inconsistent with operator precedence: A or B and C expectedly evaluates to A or (B and C) where as A * B + C in mathematical semantics evaluates to (A * B) + C.

This makes the code really difficult to reason about for people not familiar with this Dapper.FSharp exception.

It's hard to change the meaning of the operators now, as it would break existing code,
but maybe we could add some other alternative operators (&&) / (||) or (&&&) / (|||) or something alike to improve the meaning and understanding.

Question about the Where operator AND and OR

Hi, Dzoukr. Thanks for your awesome library.

I need compose multiple WHERE conditions:

    let x = select {
        for e: Customer in customerTable do
        where (e.firstname = "S")        //  1st where condition
    }

    let meetSomeCondition = true   // some condition 

    let y = 
        if not meetSomeCondition then x 
        else {
            x with 
                Where = x.Where + Column("Age", Gt 3)        //  2nd where condition
            }

    y |> conn.SelectAsync<Customer>

However, I'm confused about the operator AND & OR defined for the Where Type:

type Where =
    | Empty
    | Column of string * ColumnComparison
    | Binary of Where * BinaryOperation * Where
    | Unary of UnaryOperation * Where
    | Expr of string
    static member (+) (a, b) = Binary(a, And, b)
    static member (*) (a, b) = Binary(a, Or, b)
    static member (!!) a = Unary (Not, a)
  • The + reminds me of sum type like DU which is often interpreted as IN EITHER CASE . Sounds more like OR instead of AND.
  • While the * make me think of product type like tuple, which is often interpreted as we should have both. Sounds more like AND instead of OR.

Is there any reason why (+) is chosen for AND & (*) is chosen for OR ?

Possible feature: Add support for conditional joins over ConstantExpression

Currently, there is no way how to do conditional join.

I can use trick with multi-column (tuples):

let query = 
    select {
        for l in table<MultiJoinLeft> do
        leftJoin r in table<MultiJoinRight> on ((123, l.Key2) = (r.Key1, r.Key2)) 
        selectAll
    }

...but this fails with NotImplementedException, because of ConstantExpression.

Need to investigate how difficult this could be to add...

cc: @JordanMarr : Any thoughts on this? ๐Ÿค”

Synchronous/False Synchronous Example

Not really an issue, just would like a good syntactical example of how to use these queries in a synchronous or false synchronous (await) way. I.e - I have a select{} statement and I want to get the results collection in the same function. I am new to .NET and F# so itโ€™s not always easy to follow these things or even to know what to learn in specific cases like this! My app is very synchronous and linear - nothing can happen until the query is executed in most cases.

Discussion around Dapper vNext

It looks like Dapper vNext is going to heavily incorporate C# source generators which will break compatibility with F#.
Itโ€™s probably 3-6 months out, so this would be a good time to discuss impact and possible strategies.

See this tweet

There is also an issue on the Dapper Issues page entitled โ€œProject state?โ€ that discusses it.

Adding `option(recompile)` to a select query

Some time ago I had an issue with cached execution plan resulting in order-of-magnitude slowdown of queries. One solution to this was to force SQL Server to recalculate the plan by adding option(recompile) to the query. I managed to add it by extracting the query string using Deconstructor.select, but this meant I had to stop using SelectAsyncOption and instead reimplement the function in terms of the lower level QueryAsync.

I was wondering if 1) there's an easier way to "just add this to the end of the query", and if not, 2) would you be open to adding something along the lines of justAddThisToTheEndOfTheQuery "option(recompile)" as an escape hatch for these kinds of needs. If this is something you'd be willing to merge (with better naming of course), I can try and create a PR.

Table names with reserved names throw exception (SQL Server)

How to repro:

  • Have a table named eg. Group
  • select { table "Group" }
  • execute query

How to mitigate:

  • Add [ ] around table name in generated SQL eg. Select .... FROM [Group]
  • User is able to mitigate this by themselves only if there is no joins or where in the query by adding [] by hand to table definition in select query.

This is also an issue in other queries other than just select.

Not sure if this is an issue in other database engines.

Improve compiler support in Linq partial updates

The Linq update builder currently accepts an anonymous record for partially updating a table.
I think it is worth considering deprecating the anonymous record support (on the Linq builder only) in favor of a setColumn (or similarly named) operator that allows you to set individual columns.

Why? Better compiler support for partial updates with less chance of run-time exceptions!

  • setColumn can be constrained to 'T which will allow the F# compiler to provide type check errors at compile-time, whereas the anonymous record approach allows potential run-time errors.

For example:

update {
    for p in personTable do
    set {| FirstName = "UPDATED"; LastName = "UPDATED" |}
    where (p.Position = 1)
} 
|> conn.UpdateAsync

would become:

update {
    for p in personTable do
    setColumn p.FirstName "UPDATED" // constrained to only allow properties on `p`
    setColumn p.LastName "UPDATED"
    where (p.Position = 1)
} 
|> conn.UpdateAsync
  • A less important reason (but still a nice side effect) is that removing support for anonymous records it will greatly simplify the Linq update builder implementation by removing the extra 'U generic parameter.

Feature suggestion: Add column alias to select builder

I have a select with a join. Both tables have a column called id. I need to select one of these ids. Would be good if I can specify an alias for one of the ids so that the select can work. I think this can be accomplished by adding something like the following to the select builder.

alias "table_name.old_column_name" "new_column_name"

This is how I have implemented in my local code for MySql

type Alias = {
  oldColumnName:string
  newColumnName:string
}

type SelectQuery = {
    Table : string
    Where : Where
    OrderBy : OrderBy list
    Pagination : Pagination
    Joins : Join list
    Aliases : Alias list
}

[<CustomOperation "alias">]
    member __.Alias (state:SelectQuery, oldColumn, newColumn) = { state with Aliases = {oldColumnName = oldColumn; newColumnName = newColumn} :: state.Aliases }

let evalSelectQuery fields meta (q:SelectQuery) =
        let aliasMap = q.Aliases |> List.map (fun a -> a.newColumnName, a.oldColumnName) |> Map.ofList
        let applyAlias newSelectField = aliasMap.TryFind newSelectField |> Option.map (fun oldSelectField -> sprintf "%s AS %s" oldSelectField (inQuotes newSelectField)) |> Option.defaultValue (inQuotes newSelectField)
        let fieldNames = fields |> List.map applyAlias|> String.concat ", "
        
        // basic query
        let sb = StringBuilder(sprintf "SELECT %s FROM %s" fieldNames q.Table)
        // joins
        let joins = evalJoins q.Joins
        if joins.Length > 0 then sb.Append joins |> ignore
        // where
        let where = evalWhere meta q.Where
        if where.Length > 0 then sb.Append (sprintf " WHERE %s" where) |> ignore
        // order by
        let orderBy = evalOrderBy q.OrderBy
        if orderBy.Length > 0 then sb.Append (sprintf " ORDER BY %s" orderBy) |> ignore
        // pagination
        let pagination = evalPagination q.Pagination
        if pagination.Length > 0 then sb.Append (sprintf " %s" pagination) |> ignore
        sb.ToString()

Can't query table with optional key

Hi guys,

I'm trying to query following statement.

let enmsSichtKstEnergiezentrale = table<EnmsSicht_KstEnergiezentrale>
let enmsSichtKstErzeuger = table<EnmsSicht_KstErzeuger>
let! plants =
                select {
                    for ez in enmsSichtKstEnergiezentrale do
                    join erz in enmsSichtKstErzeuger on (ez.KstStelle = erz.USER_KstUebergeordnet.Value)
                    orderBy ez.KstStelle
                }
                |> conn.SelectAsync<EnmsSicht_KstEnergiezentrale,EnmsSicht_KstErzeuger>

Sadly I run into following error:
Could not get data "The given key was not present in the dictionary."

I have to join on a optional key. In this case erz.USER_KstUebergeordnet.Value.

Do you guys have an idea what could be the reason for that error message?

Thanks,

Tim

IsIn filter doesnt support optional values

Hi there,

we have some optional table elements and we want to use the IsIn filter on them.

In our query is concerncostcentrals.Zuordnung a optional string parameter

let branches = [Some "EZ"; Some "BGA"; Some "KW"]
let! data =
        select {
            for concerncostcentrals in enmsSichtKstKonzern do
            where ( isIn (concerncostcentrals.Zuordnung) branches )
        }
        |> conn.SelectAsync<dbo.EnmsSicht_KstKonzern>

This query the will fail with an
No mapping exists from object type Microsoft.FSharp.Core.FSharpOption1[[System.String, System.Private.CoreLib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]] to a known managed provider native type.` exception

Any idea how we can get that working?

Thanks in advance!

Please include sources in Nuget package

Hi Roman, thank you for the great library.

On my machine, using Rider and Nuget vir Paket, I can't see the F# source code (I get decompiled C#). With some other packages, like FsToolkit.Errorhandling, I can see the F# source.

Apparently this is something to do with the way it was packaged - I found this page describing source link for Nuget packages: https://devblogs.microsoft.com/nuget/introducing-source-code-link-for-nuget-packages/

It would be great if you could include a link to the source when you package up the next.

Thanks again for the great library.

Roland

Joins limit

Currently, there seems to be a limit on 2 joins. Would it be possible to increase this limit, and what would need to be done to make this happen?

Allow specifying database schema in builders

Currently all queries created by Dapper.FSharp use the default database schema eg dbo in SQL Server. Adding schema to builders it would be possible run queries to other schemas too.

eg.

select {
    schema "staging"
    table "User"
}

// SELECT ... FROM [staging].[User]

Omitting schema could work as it does now, to avoid issues with backwards compatibility.

Currently there is no way to use Dapper.FSharp to go around this other than getting the SQL out and injecting the schema there by yourself.

(Announcement) Version 4 development

From the first line of the code, my idea behind Dapper.FSharp was to create a simple and straightforward abstraction over SQL queries people tend to use most of the time. Initial idea was to create a single abstraction to cover the top three used SQL-based databases: MSSQL, MySQL, and PostgreSQL. Such a decision played well at the very beginning, but over time, and with a growing user base, there were many PRs to add more and more DB vendor-related features (e.g. #61, #65, or #70) which I had to reject for the sake of unified abstraction - I simply couldn't add features that would work only for one of the databases. And trust me on this - it never felt good to reject well-intended PRs.

So... I decided to start working on v4, which will be refactored from the ground. The main goals of this breaking change are:

  • Each DB vendor has a separate namespace with all the code (e.g. Dapper.FSharp.MSSQL)
  • Each DB vendor having own abstraction with its own computation expressions
  • Where possible, try to keep abstractions close together
  • Implement vendor-specific features on the CE syntax level
  • Revisit Deconstructor and its functionality
  • Code cleanup, testing, you know the drill ๐Ÿ˜„

ETA: End of 2022, but no promises here...

Using generic interfaces with new api

Hi Guys, me again (sorry!)

So we are still in the process of migrating all our db functions to the new API so we can keep up to date with this library.

In the previous api we had some generic 'interface'

type DbRecordSpecification<'State_DbRecord, 'EntityId, 'State> = {
    HistoryTableName  : string
    CurrentStateTableName : string
    State_ToDomainF  : 'State_DbRecord -> 'EntityId * (Version * 'State)
    State_FromDomainF  : 'EntityId * (Version * 'State) -> 'State_DbRecord
  }

With this, when we construct a new domain entity, we would simple supply a few arguments and we could then get a record out with many standard DB functions.

This is proving to be impossible (if I understand the new api correctly) with the Linq builders, due to the where clause.

As an example: Previously we could construct a select query using just the generics like so:

  let matchesEntityIdAndVersion
    (ii : DbInterfaceInput2<'State, 'EntityId, 'SuccessfulEvent , 'FailedEvent
                            , 'EntityIdDbType1, 'EntityIdDbType2, 'EntityId_DbRecord
                            , 'State_DbRecord, 'HeaderState_DbRecord, 'LineStateDbRecord
                            , 'Event_ReadRecord, 'Event_WriteRecord>)
    (entityId, version)
    : Where
    =
        match ii.EntityIdNameSpecification with
        | Single ( entityIdName, entityIdToDbTypeF ) ->
            eq entityIdName (entityId |> entityIdToDbTypeF) + eq "Version" (version |> Version.toIntO |> Option.get)
        | Tuple2 (key1Name, key2Name, entityIdToDbTypeTuple2F) ->
            let key1, key2 = entityId |> entityIdToDbTypeTuple2F
            eq key1Name key1 + eq key2Name key2 + eq "Version" (version |> Version.toIntO |> Option.get)


  let readStateStateAO
    (ii : DbInterfaceInput2<'State, 'EntityId, 'SuccessfulEvent , 'FailedEvent
                            , 'EntityIdDbType1, 'EntityIdDbType2, 'EntityId_DbRecord
                            , 'State_DbRecord, 'HeaderState_DbRecord, 'LineStateDbRecord
                            , 'Event_ReadRecord, 'Event_WriteRecord>) // takes in the interface as a parameter

    (connection : SqlConnection, transaction : SqlTransaction)
    ((entityId, version) : 'EntityId * Version)
    : Async<Option<'State>>
    = async {
        if Version.isInitial version
        then return Some ii.InitialState
        else
              let selectQuery =
                select { table ss.HistoryTableName; where (matchesEntityIdAndVersion ii (entityId, version)) }
              return!
                connection.SelectAsync<'State_DbRecord>(selectQuery, transaction)
                |> Async.AwaitTask
                |> Async.map (fun dbRecordS ->
                    dbRecordS
                    |> Seq.tryHead
                    |> Option.map (ss.State_ToDomainF >> snd >> snd) )
    }

Now, because the where no longer wants a string for the column and a value to compare, I cannot do this. Also, because it is a generic I cannot provide the "property" of the record (it is unknown at this point).

Would it be possible to "inject" a where clause into the new API rather than expecting a bool? Or can you think of any approach I could consider to use the library with a generic interface?

As always, your input is highly regarded and highly appreciated!

Add support for SQLite (Microsoft.Data.SQLite)

Since #71 was implemented and everything is separated out to their own namespace, I think it makes sense to add a Dapper.FSharp.SQLite namespace with a SQLite specific implementation.

The new implementation can support INSERT OR REPLACE (see: https://www.sqlite.org/lang_conflict.html)

Example implementation of INSERT OR REPLACE

let! conflictCount = 
    insert {
        orReplace
        into personsView
        values modified
    }
    |> conn.InsertAsync

Since SQLite does not support most data types, TypeHandlers should be created (see: https://learn.microsoft.com/en-us/dotnet/standard/data/sqlite/types)

I already have an implementation working locally.

Update to array records with new v3 `setColumn` query

I'm looking to add a new value to a column value of uuid[] and I'm running into scoping issues. I'm not able to get the current values of that column when trying to add a new value to it. This stems from the add to LINQ style column update from #40. I'd be happy to update the documentation like discussed in #50 and this use case as well when this is figured out. I wasn't really able to find any complex updates from other user searching the internet with the new syntax.

CREATE TABLE Users (
    id uuid NOT NULL PRIMARY KEY,
    items uuid[] NOT NULL DEFUALT ARRAY[]::uuid[] )
type User = 
    { Id: Guid
      Items: Guid list }

let userTable = table<User>

let addItem (conn: IDbConnection) (userId: Guid) (itemId: Guid) =
    update {
        for u in userTable do
            setColumn u.Items (List.Cons(itemId, u.Items)
            where (id = userId)
    }
error FS0039: The value or constructor 'u' is not defined

The equivalent SQL command I'm trying to emulate is

UPDATE users
SET items = array_append(items, @item)
WHERE id = @id

innerJoin compilation error in v2.2.0+

The code compiled OK on v2.1.0:

select {
    table "Players"
    innerJoin "Games" "Id" "GameId"
    where (eq "Players.Id" playerId.Value)
}

However with v2.2.0 and v.2.3.0, the compiler reported an error as below:

[FS3087] The custom operation 'innerJoin' refers to a method which is overloaded. The implementations of custom operations may not be overloaded.

I tried with innerJoin "Games" [ ("Id", "GameId") ] but the error is still there.

Autoincremented ID Columns

Having a bit of trouble finding information on autoincremented identity columns using this setup. Is there a particular way to go about this?
I tested using record types with the Id field set to -1, 0, and None.

Is it just more correct to use guid types for this when using this approach?

Support for snake_case column names

Hi,

the preferred naming convention for postgres databases calls for table/column names to be using snake_case, since Dapper allows reading such columns with Dapper.DefaultTypeMap.MatchNamesWithUnderscores <- true could we also somehow add an option for names in record fields to be interpreted as snake_case?

Example:

let r = {
    Id = Guid.NewGuid()
    FirstName = "Works"
    LastName = "Great"
    DateOfBirth = DateTime.Today
    Position = 1
}

let sql, values =
    insert {
        table "person"
        value r
    } |> Deconstructor.insert

printfn "%s" sql 
// INSERT INTO person (id, first_name, last_name, position, date_of_birth) 
// VALUES (@Id0, @FirstName0, @LastName0, @Position0, @DateOfBirth0)"

Implementation of custom operations may not be overloaded

Hey guys, I was looking to join on multiple columns and found issue 36. I actually want a left outer join on multiple columns, not an inner join, but that is besides the point.

When I try to upgrade to a later version than 2.1.0 it breaks all of my existing (working) code, with the following error message:

The custom operation 'orderBy' refers to a method which is overloaded. The implementations of custom operations may not be overloaded.F# Compiler(3087)

It appears the F# compiler does not allow overloads for custom operations, and after version 2.1.0 this seems to be how you have implemented both more orderBy stuff and the aforementioned innerJoin on multiple columns. I really appreciate these new features, but how am I supposed to use it?

Support for ilike in postgresql

Just like like there is something called ilike which is cooperation but case insensitive. It would be great if you can support that as well along side like

if you are Ok. I can give merge request.

New comparison operators

There are a handful of new comparison operators that I implemented in SqlHydra that you may be interested in:

isIn or |=|
isNotIn or |<>|
like or =%
notLike or <>%

(They are the same operators used in SqlProvider, so they should be familiar to some people already).

How to do use IN operator in LINQ queries?

I'm trying to convert SqlProvider queries to Dapper.FSharp queries.

In Sqlprovider you can use |=| as an IN operator in the where clause.

Would be nice if I could do something like that with Dapper.FSharp.

I tried this which obviously does not work:

let getIdsAndDescriptions (conn: IDbConnection)=
    task {
        let! ids =
            select {
            for e in enmsTabelle do
            join datapoints  in enmsSichtDatenpunkte on (e.Sakto = datapoints.Sakto)
            where (datapoints.Mandant = int16 999
                && datapoints.USER_KstEbene |=| [|"Contracting";"BGA";"KW"|])
            distinct
             }
            |> conn.SelectAsync<{| Saktobez: string ; Sakto : string ; USER_KstEbene : string |}>
        return ids |> Seq.toArray
    }

Any idea how to implement that?

InsertOutput in MySQL

Would it be possible to implement an InsertOutput option for MySQL? I want to build a library on top of this one, and whilst I personally use Postgres it would probably be good to have it universal.

This could be done with another select query immediately after using LAST_INSERT_ID()
Though Iโ€™m not sure on best practice there.

Or could even implement Returning which does work in MariaDb I believe, so gives a little more coverage.

Missing method exception: Value binary comparison (but should be propery)

Hi,
We want to stay up to date with the changes in V3, this means we need to rewrite all our current db functions to use LinqBuilders, but we have run into a hurdle and I am unsure why it is happening.

Given :

    open Dapper.FSharp.MSSQL
    open Dapper.FSharp.LinqBuilders

    let companyTable = table'<ReadRecord> "Company" |> inSchema "Common"

    let getRecordByGuidV2AO (connection : SqlConnection, transaction) guid =

      let selectQuery = select { for i in companyTable do
                                 where (isIn i.CompanyGuid [guid])}
      connection.SelectAsync<ReadRecord> (selectQuery, transaction)
      |> Async.AwaitTask
      |> Async.map (Seq.tryHead >> Option.map ReadRecord.toDomainX)

    let getRecordByGuidAO (connection : SqlConnection, transaction) guid =

      let selectQuery = select { for i in companyTable do
                                 where (i.CompanyGuid = guid )}
      connection.SelectAsync<ReadRecord> (selectQuery, transaction)
      |> Async.AwaitTask
      |> Async.map (Seq.tryHead >> Option.map ReadRecord.toDomainX)

The function getRecordByGuidV2AO works and tests successfully, however, the function getRecordByGuidAO gives expection which relates to this match. I can see the test works : simple select, the only difference being that we are passing the comparison value as a parameter to the function.
I must be missing something silly, but cannot see it.

Query expressions

Iโ€™ve been trying to find the perfect ORM to complement SqlHydra.
Unfortunately, I canโ€™t find much in the way of current F# ORMs with strongly typed query expressions that work with records.

Dapper.FSharp is my favorite candidate so far due to its simplicity and the fact that it targets F# first and foremost and has great support for records. In conjunction with SqlHydra generating the entity records, I think it would be very nice if it featured strongly typed query expressions. This would make it the perfect complement.

So I just wanted to check to see if this is a PR that you might be interested in. Iโ€™m imagining a strongly typed version of the select query expression that would feed into the current select CE.

If you would prefer to keep Dapper.FSharp super simple and avoid adding this, I totally understand. In that case, I think this could also be a good candidate for a separate NuGet package that augments Dapper.FSharp as a dependency (similar to libs like Dapper.Contrib).
Of course that also could get kind of ridiculous since it would essentially be an addon to an addon. ๐Ÿ˜„

Structures in columns

I'm trying out some proof of concepts, using the example from another F# implementation.
There is a user database, with name and passwort, where the pw is a discriminated union - either plain text or a hash with its salt.

As I am questioning the sense of storing plain text pws, I've removed the union, so I want to store the following in the db:

type Password = {
  Hash: string
  Salt: string
}
type User = {
  Id: int64 option
  Name: string
  Password: Password
}

Is there an example how to store this in a database-agnostic way?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.