Skip to content

tforkmann/Fumble

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Fumble

Functional wrapper around plain old Sqlite to simplify data access when talking to SQLite databases.

Available Packages:

Library Version
Fumble nuget - Fumble

Install

# nuget client
dotnet add package Fumble

# or using paket
.paket/paket.exe add Fumble --project path/to/project.fsproj

Query a table

open Fumble

// get the connection from the environment
let connectionString() = Env.getVar "app_db"

type User = { Id: int; Username: string }

let getUsers() : Result<User list, exn> =
    connectionString()
    |> Sql.connect
    |> Sql.query "SELECT * FROM dbo.[Users]"
    |> Sql.execute (fun read ->
        {
            Id = read.int "user_id"
            Username = read.string "username"
        })

Handle null values from table columns:

open Fumble

// get the connection from the environment
let connectionString() = Env.getVar "app_db"

type User = { Id: int; Username: string; LastModified : Option<DateTime> }

let getUsers() : Result<User list, exn> =
    connectionString()
    |> Sql.connect
    |> Sql.query "SELECT * FROM dbo.[users]"
    |> Sql.execute(fun read ->
        {
            Id = read.int "user_id"
            Username = read.string "username"
            // Notice here using `orNone` reader variants
            LastModified = read.dateTimeOrNone "last_modified"
        })

Providing default values for null columns:

open Fumble

// get the connection from the environment
let connectionString() = Env.getVar "app_db"

type User = { Id: int; Username: string; Biography : string }

let getUsers() : Result<User list, exn> =
    connectionString()
    |> Sql.connect
    |> Sql.query "select * from dbo.[users]"
    |> Sql.execute (fun read ->
        {
            Id = read.int "user_id";
            Username = read.string "username"
            Biography = defaultArg (read.stringOrNone "bio") ""
        })

Execute a parameterized query

open Fumble

// get the connection from the environment
let connectionString() = Env.getVar "app_db"

// get product names by category
let productsByCategory (category: string) : Result<string list, exn> =
    connectionString()
    |> Sql.connect
    |> Sql.query "SELECT name FROM dbo.[Products] where category = @category"
    |> Sql.parameters [ "@category", Sql.string category ]
    |> Sql.execute (fun read -> read.string "name")

Supported Data Types

Basic Types

F# Type Parameter Reader
int Sql.int read.int
int16 Sql.int16 read.int16
int64 Sql.int64 read.int64
string Sql.string read.string
bool Sql.bool read.bool
decimal Sql.decimal read.decimal
double Sql.double read.double
float Sql.double read.float
float32 Sql.double read.float32
Guid Sql.uniqueidentifier read.uniqueidentifier
byte[] Sql.bytes read.bytes
DateTime Sql.dateTime read.dateTime
DateTimeOffset Sql.dateTimeOffset read.dateTimeOffset

New in v2.0

F# Type Parameter Reader Storage
TimeSpan Sql.timeSpan read.timeSpan INTEGER (ticks)
DateOnly Sql.dateOnly read.dateOnly TEXT (yyyy-MM-dd)
TimeOnly Sql.timeOnly read.timeOnly INTEGER (ticks)
byte Sql.byte read.tinyint INTEGER
uint32 Sql.uint32 read.uint32 INTEGER
uint64 Sql.uint64 read.uint64 INTEGER

All types have OrNone variants for nullable columns (e.g., Sql.intOrNone, read.intOrNone).

New in v2.0: Additional Features

Execute Scalar Queries

// Get a single value
let count =
    connectionString()
    |> Sql.connect
    |> Sql.query "SELECT COUNT(*) FROM Users"
    |> Sql.executeScalar<int64>
    // Returns: Result<int64 option, exn>

// Check if data exists
let exists =
    connectionString()
    |> Sql.connect
    |> Sql.query "SELECT 1 FROM Users WHERE Username = @name"
    |> Sql.parameters [ "@name", Sql.string "john" ]
    |> Sql.executeExists
    // Returns: Result<bool, exn>

// Get count directly
let userCount =
    connectionString()
    |> Sql.connect
    |> Sql.query "SELECT COUNT(*) FROM Users"
    |> Sql.executeCount
    // Returns: Result<int64, exn>

Pagination

// Using take (LIMIT)
let firstTen =
    connectionString()
    |> Sql.connect
    |> Sql.query "SELECT * FROM Users ORDER BY Id"
    |> Sql.take 10
    |> Sql.execute (fun read -> read.string "Username")

// Using paginate (LIMIT + OFFSET)
let page2 =
    connectionString()
    |> Sql.connect
    |> Sql.query "SELECT * FROM Users ORDER BY Id"
    |> Sql.paginate 2 10  // page 2, 10 items per page
    |> Sql.execute (fun read -> read.string "Username")

In-Memory Database

// Create an in-memory database
Sql.connectInMemory ()
|> Sql.query "CREATE TABLE Test (Id INTEGER PRIMARY KEY, Name TEXT)"
|> Sql.executeNonQuery

// Shared in-memory database (accessible from multiple connections)
Sql.connectInMemoryShared "mydb"
|> Sql.query "SELECT * FROM Test"
|> Sql.execute (fun read -> read.string "Name")

SQLite Pragmas and Introspection

// Get SQLite version
let version =
    connectionString()
    |> Sql.connect
    |> Sql.sqliteVersion
    // Returns: Result<string option, exn>

// List all tables
let tables =
    connectionString()
    |> Sql.connect
    |> Sql.listTables
    // Returns: Result<string list, exn>

// Get table schema info
let columns =
    connectionString()
    |> Sql.connect
    |> Sql.tableInfo "Users"
    // Returns column info with Name, Type, NotNull, DefaultValue, IsPrimaryKey

// Enable WAL mode for better concurrency
connectionString()
|> Sql.connect
|> Sql.enableWalMode

// Enable foreign keys
connectionString()
|> Sql.connect
|> Sql.enableForeignKeys

// Database maintenance
connectionString()
|> Sql.connect
|> Sql.vacuum  // Rebuild database file

connectionString()
|> Sql.connect
|> Sql.analyze  // Update statistics

Bulk Operations

// Bulk delete
connectionString()
|> Sql.connect
|> Sql.bulkDelete "Users" "Id" [1; 2; 3]
// Deletes users with Id 1, 2, or 3

// Upsert (INSERT OR REPLACE)
type User = { Id: int; Name: string; Email: string }

let users = [
    { Id = 1; Name = "John"; Email = "john@example.com" }
    { Id = 2; Name = "Jane"; Email = "jane@example.com" }
]

connectionString()
|> Sql.connect
|> Sql.upsert "Users" users
// Inserts or replaces based on primary key

Auto-generate CREATE TABLE

type User = {
    Id: int
    Username: string
    Email: string option
    CreatedAt: DateTime
}

// Creates: CREATE TABLE IF NOT EXISTS [Users]
//    ([Id] INTEGER,
//     [Username] TEXT,
//     [Email] TEXT NULL,
//     [CreatedAt] TEXT)
connectionString()
|> Sql.connect
|> Sql.commandCreate<User> "Users"
|> Sql.executeCommand

Executing a stored procedure with parameters

open Fumble

// get the connection from the environment
let connectionString() = Env.getVar "app_db"

// check whether a user exists or not
let userExists (username: string) : Async<Result<bool, exn>> =
    async {
        return!
            connectionString()
            |> Sql.connect
            |> Sql.storedProcedure "user_exists"
            |> Sql.parameters [ "@username", Sql.string username ]
            |> Sql.execute (fun read -> read.bool 0)
            |> function
                | Ok [ result ] -> Ok result
                | Error error -> Error error
                | unexpected -> failwithf "Expected result %A"  unexpected
    }

Running Tests locally

You only need a working local Sqlite. The tests will create databases when required and dispose of them at the end of the each test.

dotnet run --project tests/Fumble.Tests/FumbleTests.fsproj

About

Thin F# API for Sqlite for easy data access to sqlite database with functional seasoning on top

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 4

  •  
  •  
  •  
  •