Home
Softono
LiteDB.FSharp

LiteDB.FSharp

Open source MIT F#
187
Stars
20
Forks
8
Issues
11
Watchers
3 years
Last Commit

About LiteDB.FSharp

LiteDB.FSharp provides F support for LiteDB, an embedded NoSQL database for .NET. It includes serialization utilities that enable LiteDB to handle F types such as records, discriminated unions, and maps. The library offers type-safe query expressions through F quotations and provides a custom FSharpBsonMapper for LiteDatabase initialization. Records used as documents require a primary key field named Id or id, which maps to id. The library supports standard operations including inserting documents, querying by ID, querying by field values, querying by discriminated union cases, and range queries on fields like DateTime. Advanced features include full search using quoted expressions for custom filtering logic, as well as Query.Where for filtering documents based on deserialized BsonValue fields. It works with nested discriminated unions and supports both simple equality queries and complex custom filtering predicates.

Platforms

Web Self-hosted

Languages

F#

Links

LiteDB.FSharp Build Status Nuget

F# Support for LiteDB

This package relies on LiteDB 4.14 >= version > 5.0 Support for v5 is work in progress and might require a full rewrite.

LiteDB.FSharp provides serialization utilities making it possible for LiteDB to understand F# types such as records, unions, maps etc. with support for type-safe query expression through F# quotations

Usage

LiteDB.FSharp comes with a custom BsonMapper called FSharpBsonMapper that you would pass to a LiteDatabase instance during initialization:

open LiteDB
open LiteDB.FSharp
open LiteDB.FSharp.Extensions

let mapper = FSharpBsonMapper()
use db = new LiteDatabase("simple.db", mapper)

LiteDB.FSharp is made mainly to work with records as representations of the persisted documents. The library requires that records have a primary key called Id or id. This field is then mapped to _id when converted to a bson document for indexing.

type Genre = Rock | Pop | Metal

type Album = {
    Id: int
    Name: string
    DateReleased: DateTime
    Genre: Genre
}

Get a typed collection from the database:

let albums = db.GetCollection<Album>("albums")

Insert documents

let metallica = 
    { Id = 1; 
      Name = "Metallica";
      Genre = Metal;
      DateReleased = DateTime(1991, 8, 12) }

albums.Insert(metallica)

Query one document by Id

// result : Album
let result = albums.findOne <@ fun album -> album.Id = 1 @> 

// OR
let id = BsonValue(1)
// result : Album
let result = albums.FindById(id)

Query many documents depending on the value of a field

// metallicaAlbums : Seq<Album>
let metallicaAlbums = albums.findMany <@ fun album -> album.Name = "Metallica" @>
// OR
let name = BsonValue("Metallica")
let query = Query.EQ("Name", name)
// metallicaAlbums : Seq<Album>
let metallicaAlbums = albums.Find(query)

Query documents by value of discriminated union

// find all albums where Genre = Rock
// rockAlbums : Seq<Album>
let rockAlbums = albums.findMany <@ fun album -> album.Genre = Rock @>

// OR 

let genre = BsonValue("Rock")
let query = Query.EQ("Genre", genre)
// rockAlbums : Seq<Album>
let rockAlbums = albums.Find(query)

Query documents between or time intervals

// find all albums released last year
let now = DateTime.Now
let dateFrom = DateTime(now.Year - 1, 01, 01) |> BsonValue
let dateTo = DateTime(now.Year, 01, 01) |> BsonValue
let query = Query.Between("DateReleased", dateFrom, dateTo)
// albumsLastYear : Seq<Album>
let albumsLastYear = albums.Find(query)

Customized Full Search using quoted expressions

// Filtering albums released a year divisble by 5
// filtered : Seq<Album>
let filtered = 
    albums.fullSearch 
        <@ fun album -> album.DateReleased @> 
        (fun dateReleased -> dateReleased.Year % 5 = 0)

Customized Full Search using Query.Where

The function Query.Where expects a field name and a filter function of type BsonValue -> bool. You can deserialize the BsonValue using Bson.deserializeField<'t> where 't is the type of the serialized value.

// Filtering albums released a year divisble by 5
let searchQuery = 
    Query.Where("DateReleased", fun bsonValue ->
        // dateReleased : DateTime
        let dateReleased = Bson.deserializeField<DateTime> bsonValue
        let year = dateReleased.Year
        year % 5 = 0
    )

let searchResult = albums.Find(searchQuery)

Query.Where: Filtering documents by matching with values of a nested DU

type Shape = 
    | Circle of float
    | Rect of float * float
    | Composite of Shape list

type RecordWithShape = { Id: int; Shape: Shape }

let records = db.GetCollection<RecordWithShape>("shapes")

let shape = 
    Composite [ 
      Circle 2.0;
      Composite [ Circle 4.0; Rect(2.0, 5.0) ]
    ]

let record = { Id = 1; Shape = shape }
records.Insert(record) |> ignore

let searchQuery = 
    Query.Where("Shape", fun bsonValue -> 
        let shapeValue = Bson.deserializeField<Shape> bsonValue
        match shapeValue with
        | Composite [ Circle 2.0; other ] -> true
        | otherwise -> false
    )
records.Find(searchQuery)
|> Seq.length
|> function 
    | 1 -> pass() // passed!
    | n -> fail()

Id auto-incremented

Add CLIMutableAttribute to record type and set Id 0

[<CLIMutable>]
 type Album = {
    Id: int
    Name: string
    DateReleased: DateTime
    Genre: Genre
}
let metallica = 
    { Id = 0; 
      Name = "Metallica";
      Genre = Metal;
      DateReleased = DateTime(1991, 8, 12) }

DbRef

just as https://github.com/mbdavid/LiteDB/wiki/DbRef

open LiteDB.FSharp.Linq

[<CLIMutable>]
type Company=
  { Id : int
    Name : string}   

[<CLIMutable>]    
type Order=
  { Id :int
    Company :Company }

let mapper = FSharpBsonMapper()
mapper.DbRef<Order,_>(fun c -> c.Company)

Inheritence

Item1 and Item2 are inherited from IItem

we must register the type relations first globally

FSharpBsonMapper.RegisterInheritedConverterType<IItem,Item1>()
FSharpBsonMapper.RegisterInheritedConverterType<IItem,Item2>()

By conversion, The inherited type must has mutable field for serializable and deserializable

val mutable Id : int

Note: Because json converter find inherited type by comparing the fields names from inherited type and database

let findType (jsonFields: seq<string>) =
    inheritedTypes |> Seq.maxBy (fun tp ->
        let fields = tp.GetFields() |> Seq.map (fun fd -> fd.Name)
        let fieldsLength = Seq.length fields
        (jsonFields |> Seq.filter(fun jsonField ->
            Seq.contains jsonField fields
        )
        |> Seq.length),-fieldsLength
    )        

This means that we should not implement the some interface with different fields For example,we should not do below implementations

type Item1 =

    val mutable Id : int
    val mutable Art : string
    val mutable Name : string
    val mutable Number : int

    interface IItem with 
        member this.Art = this.Art
        member this.Id = this.Id
        member this.Name = this.Name
        member this.Number = this.Number

/// unexpected codes
type Item2 =

    val mutable Id2 : int
    val mutable Art2 : string
    val mutable Name2 : string
    val mutable Number2 : int

    interface IItem with 
        member this.Art = this.Art2
        member this.Id = this.Id2
        member this.Name = this.Name2
        member this.Number = this.Number2

/// expected codes
type Item2 =

    val mutable Id : int
    val mutable Art : string
    val mutable Name : string
    val mutable Number : int

    interface IItem with 
        member this.Art = this.Art
        member this.Id = this.Id
        member this.Name = this.Name
        member this.Number = this.Number

Full sample codes:

/// classlibray.fs
[<CLIMutable>]    
type EOrder=
  { Id: int
    Items : IItem list
    OrderNumRange: string }   


/// consumer.fs
type Item1 =
    /// val mutable will make field serializable and deserializable
    val mutable Id : int
    val mutable Art : string
    val mutable Name : string
    val mutable Number : int


    interface IItem with 
        member this.Art = this.Art
        member this.Id = this.Id
        member this.Name = this.Name
        member this.Number = this.Number
    val mutable Barcode : string

    interface IBarcode with 
        member this.Barcode = this.Barcode    

    /// type constructor 
    new (id, art, name, number, barcode) =
        { Id = id; Art = art; Name = name; Number = number; Barcode = barcode }

type Item2 = 
    val mutable Id : int
    val mutable Art : string
    val mutable Name : string
    val mutable Number : int

    interface IItem with 
        member this.Art = this.Art
        member this.Id = this.Id
        member this.Name = this.Name
        member this.Number = this.Number

    val mutable Size : int
    interface ISize with 
        member this.Size = this.Size 
    val mutable Color : string

    interface IColor with 
        member this.Color = this.Color 

    new (id, art, name, number, size, color) =
        { Id = id; Art = art; Name = name; Number = number; Size = size; Color = color }

FSharpBsonMapper.RegisterInheritedConverterType<IItem,Item1>()
FSharpBsonMapper.RegisterInheritedConverterType<IItem,Item2>()

let item1 = 
    Item1 ( 
        id = 0,
        art = "art",
        name = "name",
        number = 1000,
        barcode = "7254301" 
    )

let item2 = 
    Item2 ( 
        id = 0,
        art = "art",
        name = "name",
        number = 1000,
        color = "red" ,
        size = 39 
    )

let eorder = { Id = 1; Items = [item1;item2];  OrderNumRange = "" }

let queryedEOrder =
    db 
    |> LiteRepository.insertItem eorder
    |> LiteRepository.query<EOrder> 
    |> LiteQueryable.first

match queryedEOrder.Items with 
| [item1;item2] -> 
    match item1,item2 with 
    | :? IBarcode,:? IColor -> 
        pass()
    | _ -> fail()    
| _ -> fail()